MM-26329 Fix Channel info guests info in landscape (#4692) (#4702)

* MM-26329 Fix Channel info guests info in landscape

* Channnel info refactor

* Rename canEditChannel to canEdit in connected component

(cherry picked from commit 55cfce9b89)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Mattermost Build 2020-08-19 10:18:46 -04:00 committed by GitHub
parent c4e23af279
commit 55511e1fdb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 3269 additions and 1120 deletions

View file

@ -29,10 +29,9 @@ type BatchAction = {
export type Action = GenericAction | Thunk | BatchAction | ActionFunc;
export type ActionResult = {
data: any;
} | {
error: any;
};
data?: any;
error?: any;
}
export type DispatchFunc = (action: Action, getState?: GetStateFunc | null) => Promise<ActionResult>;
export type ActionFunc = (dispatch: DispatchFunc, getState: GetStateFunc) => Promise<ActionResult|ActionResult[]> | ActionResult;

View file

@ -13,3 +13,8 @@ export type GeneralState = {
serverVersion: string;
timezones: Array<string>;
};
export type FormattedMsg = {
id: string;
defaultMessage: string;
};

View file

@ -2,4 +2,5 @@
// See LICENSE.txt for license information.
declare module 'gfycat-sdk';
declare module 'remote-redux-devtools';
declare module 'redux-action-buffer';
declare module 'redux-action-buffer';
declare module 'react-intl';

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`channel_info should match snapshot 1`] = `
exports[`channelInfo should match snapshot 1`] = `
<View
style={
Object {
@ -75,15 +75,9 @@ exports[`channel_info should match snapshot 1`] = `
}
>
<React.Fragment>
<channelInfoRow
action={[Function]}
defaultMessage="Favorite"
detail={false}
icon="star-o"
<Connect(Favorite)
channelId="1234"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="mobile.routes.channelInfo.favorite"
theme={
Object {
"awayIndicator": "#ffbc42",
@ -113,26 +107,8 @@ exports[`channel_info should match snapshot 1`] = `
"type": "Mattermost",
}
}
togglable={true}
/>
<View
style={
Object {
"backgroundColor": undefined,
"height": 1,
"marginHorizontal": 15,
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Mute channel"
detail={false}
icon="bell-slash-o"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="channel_notifications.muteChannel.settings"
<Separator
theme={
Object {
"awayIndicator": "#ffbc42",
@ -162,26 +138,10 @@ exports[`channel_info should match snapshot 1`] = `
"type": "Mattermost",
}
}
togglable={true}
/>
<View
style={
Object {
"backgroundColor": undefined,
"height": 1,
"marginHorizontal": 15,
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Ignore @channel, @here, @all"
detail={false}
icon="at"
<Connect(Mute)
channelId="1234"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="channel_notifications.ignoreChannelMentions.settings"
theme={
Object {
"awayIndicator": "#ffbc42",
@ -211,25 +171,9 @@ exports[`channel_info should match snapshot 1`] = `
"type": "Mattermost",
}
}
togglable={true}
userId="1234"
/>
<View
style={
Object {
"backgroundColor": undefined,
"height": 1,
"marginHorizontal": 15,
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Pinned Posts"
image={1}
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="channel_header.pinnedPosts"
<Separator
theme={
Object {
"awayIndicator": "#ffbc42",
@ -259,228 +203,202 @@ exports[`channel_info should match snapshot 1`] = `
"type": "Mattermost",
}
}
togglable={false}
/>
<React.Fragment>
<View
style={
Object {
"backgroundColor": undefined,
"height": 1,
"marginHorizontal": 15,
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Manage Members"
detail={2}
icon="users"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="channel_header.manageMembers"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={false}
/>
</React.Fragment>
<React.Fragment>
<View
style={
Object {
"backgroundColor": undefined,
"height": 1,
"marginHorizontal": 15,
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Add Members"
icon="user-plus"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="channel_header.addMembers"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={false}
/>
</React.Fragment>
<React.Fragment>
<View
style={
Object {
"backgroundColor": undefined,
"height": 1,
"marginHorizontal": 15,
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Convert to Private Channel"
icon="lock"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="mobile.channel_info.convert"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={false}
/>
</React.Fragment>
<React.Fragment>
<View
style={
Object {
"backgroundColor": undefined,
"height": 1,
"marginHorizontal": 15,
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Edit Channel"
icon="edit"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="mobile.channel_info.edit"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={false}
/>
</React.Fragment>
</React.Fragment>
<React.Fragment>
<View
style={
Object {
"backgroundColor": undefined,
"height": 1,
"marginHorizontal": 15,
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Leave Channel"
icon="sign-out"
<Connect(IgnoreMentions)
channelId="1234"
isLandscape={false}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
<Separator
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
<Connect(Pinned)
channelId="1234"
isLandscape={false}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
<Connect(ManageMembers)
isLandscape={false}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
<Connect(AddMembers)
isLandscape={false}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
<Connect(ConvertPrivate)
isLandscape={false}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
<Connect(EditChannel)
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="navbar.leave"
theme={
Object {
"awayIndicator": "#ffbc42",
@ -510,40 +428,52 @@ exports[`channel_info should match snapshot 1`] = `
"type": "Mattermost",
}
}
togglable={false}
/>
</React.Fragment>
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"borderBottomColor": undefined,
"borderBottomWidth": 1,
"borderTopColor": undefined,
"borderTopWidth": 1,
},
Object {
"borderBottomColor": undefined,
"borderBottomWidth": 1,
"borderTopColor": undefined,
"borderTopWidth": 1,
"marginTop": 40,
},
]
Object {
"marginTop": 40,
}
}
>
<channelInfoRow
action={[Function]}
defaultMessage="Archive Channel"
icon="archive"
iconColor="#CA3B27"
<Connect(Leave)
close={[Function]}
isLandscape={false}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
<Connect(Archive)
close={[Function]}
isLandscape={false}
rightArrow={true}
shouldRender={true}
textColor="#CA3B27"
textId="mobile.routes.channelInfo.delete_channel"
theme={
Object {
"awayIndicator": "#ffbc42",
@ -573,7 +503,6 @@ exports[`channel_info should match snapshot 1`] = `
"type": "Mattermost",
}
}
togglable={false}
/>
</View>
</ScrollView>

View file

@ -395,9 +395,12 @@ exports[`channel_info_header should match snapshot when DM and hasGuests and is
</View>
<View
style={
Object {
"marginTop": 15,
}
Array [
Object {
"marginTop": 15,
},
null,
]
}
>
<View
@ -1045,9 +1048,12 @@ exports[`channel_info_header should match snapshot when GM and hasGuests 1`] = `
</View>
<View
style={
Object {
"marginTop": 15,
}
Array [
Object {
"marginTop": 15,
},
null,
]
}
>
<View
@ -1717,9 +1723,12 @@ exports[`channel_info_header should match snapshot when public channel and hasGu
</View>
<View
style={
Object {
"marginTop": 15,
}
Array [
Object {
"marginTop": 15,
},
null,
]
}
>
<View

View file

@ -0,0 +1,76 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChannelInfo -> Add Members should match snapshot 1`] = `
<React.Fragment>
<Separator
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Add Members"
icon="user-plus"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="channel_header.addMembers"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={false}
/>
</React.Fragment>
`;

View file

@ -0,0 +1,55 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallowWithIntl} from 'test/intl-test-helper';
import Preferences from '@mm-redux/constants/preferences';
import AddMembers from './add_members';
jest.mock('@utils/theme', () => {
const original = jest.requireActual('../../../utils/theme');
return {
...original,
changeOpacity: jest.fn(),
};
});
describe('ChannelInfo -> Add Members', () => {
const baseProps = {
canManageUsers: true,
groupConstrained: false,
isLandscape: false,
theme: Preferences.THEMES.default,
};
test('should match snapshot', () => {
const wrapper = shallowWithIntl(
<AddMembers
{...baseProps}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should render null if cannot manage members', () => {
const wrapper = shallowWithIntl(
<AddMembers
{...baseProps}
canManageUsers={false}
/>,
);
expect(wrapper.getElement()).toBeNull();
});
test('should render null if channel is constrained to groups', () => {
const wrapper = shallowWithIntl(
<AddMembers
{...baseProps}
groupConstrained={true}
/>,
);
expect(wrapper.getElement()).toBeNull();
});
});

View file

@ -0,0 +1,55 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {intlShape} from 'react-intl';
import {goToScreen} from '@actions/navigation';
import {Theme} from '@mm-redux/types/preferences';
import ChannelInfoRow from '@screens/channel_info/channel_info_row';
import Separator from '@screens/channel_info/separator';
import {t} from '@utils/i18n';
import {preventDoubleTap} from '@utils/tap';
interface AddMembersProps {
canManageUsers: boolean;
groupConstrained: boolean;
isLandscape: boolean;
theme: Theme;
}
export default class AddMembers extends PureComponent<AddMembersProps> {
static contextTypes = {
intl: intlShape.isRequired,
};
goToChannelAddMembers = preventDoubleTap(() => {
const {intl} = this.context;
const screen = 'ChannelAddMembers';
const title = intl.formatMessage({id: 'channel_header.addMembers', defaultMessage: 'Add Members'});
goToScreen(screen, title);
});
render() {
const {canManageUsers, groupConstrained, isLandscape, theme} = this.props;
if (canManageUsers && !groupConstrained) {
return (
<>
<Separator theme={theme}/>
<ChannelInfoRow
action={this.goToChannelAddMembers}
defaultMessage='Add Members'
icon='user-plus'
textId={t('channel_header.addMembers')}
theme={theme}
isLandscape={isLandscape}
/>
</>
);
}
return null;
}
}

View file

@ -0,0 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {canManageChannelMembers, getCurrentChannel} from '@mm-redux/selectors/entities/channels';
import AddMembers from './add_members';
function mapStateToProps(state) {
const currentChannel = getCurrentChannel(state);
let canManageUsers = currentChannel?.id ? canManageChannelMembers(state) : false;
if (currentChannel.group_constrained) {
canManageUsers = false;
}
return {
canManageUsers,
groupConstrained: currentChannel.group_constrained,
};
}
export default connect(mapStateToProps)(AddMembers);

View file

@ -0,0 +1,155 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChannelInfo -> Archive should match snapshot for Archive Channel 1`] = `
<React.Fragment>
<Separator
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Archive Channel"
icon="archive"
iconColor="#CA3B27"
isLandscape={false}
rightArrow={false}
shouldRender={true}
textColor="#CA3B27"
textId="mobile.routes.channelInfo.delete_channel"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={false}
/>
</React.Fragment>
`;
exports[`ChannelInfo -> Archive should match snapshot for Unarchive Channel 1`] = `
<React.Fragment>
<Separator
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Unarchive Channel"
icon="archive"
iconColor="#CA3B27"
isLandscape={false}
rightArrow={false}
shouldRender={true}
textColor="#CA3B27"
textId="mobile.routes.channelInfo.unarchive_channel"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={false}
/>
</React.Fragment>
`;

View file

@ -0,0 +1,67 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallowWithIntl} from 'test/intl-test-helper';
import Preferences from '@mm-redux/constants/preferences';
import Archive from './archive';
jest.mock('@utils/theme', () => {
const original = jest.requireActual('../../../utils/theme');
return {
...original,
changeOpacity: jest.fn(),
};
});
describe('ChannelInfo -> Archive', () => {
const baseProps = {
canArchive: true,
canUnarchive: false,
channelId: '123',
close: jest.fn(),
deleteChannel: jest.fn(),
displayName: 'Test Channel',
getChannel: jest.fn(),
handleSelectChannel: jest.fn(),
isLandscape: false,
isPublic: true,
unarchiveChannel: jest.fn(),
selectPenultimateChannel: jest.fn(),
teamId: 'team-123',
theme: Preferences.THEMES.default,
viewArchivedChannels: true,
};
test('should match snapshot for Archive Channel', () => {
const wrapper = shallowWithIntl(
<Archive
{...baseProps}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for Unarchive Channel', () => {
const wrapper = shallowWithIntl(
<Archive
{...baseProps}
canUnarchive={true}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot Not render Archive', () => {
const wrapper = shallowWithIntl(
<Archive
{...baseProps}
canArchive={false}
canUnarchive={false}
/>,
);
expect(wrapper.getElement()).toBeNull();
});
});

View file

@ -0,0 +1,177 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {Alert} from 'react-native';
import {intlShape} from 'react-intl';
import {ActionResult} from '@mm-redux/types/actions';
import {FormattedMsg} from '@mm-redux/types/general';
import {Theme} from '@mm-redux/types/preferences';
import ChannelInfoRow from '@screens/channel_info/channel_info_row';
import Separator from '@screens/channel_info/separator';
import {alertErrorWithFallback} from '@utils/general';
import {t} from '@utils/i18n';
import {preventDoubleTap} from '@utils/tap';
interface ArchiveProps {
canArchive: boolean;
canUnarchive: boolean;
channelId: string;
close: (redirect: boolean) => void;
deleteChannel: (channelId: string) => Promise<ActionResult>;
displayName: string;
getChannel: (channelId: string) => Promise<ActionResult>;
handleSelectChannel: (channelId: string) => Promise<ActionResult>;
isLandscape: boolean;
isPublic: boolean;
unarchiveChannel: (channelId: string) => Promise<ActionResult>;
selectPenultimateChannel: (channelId: string) => Promise<ActionResult>;
teamId: string;
theme: Theme;
viewArchivedChannels: boolean;
}
export default class Archive extends PureComponent<ArchiveProps> {
static contextTypes = {
intl: intlShape.isRequired,
};
alertAndHandleYesAction = (title: FormattedMsg, message: FormattedMsg, onPressAction: () => void) => {
const {formatMessage} = this.context.intl;
const {displayName, isPublic} = this.props;
// eslint-disable-next-line multiline-ternary
const term = isPublic ? formatMessage({id: 'mobile.channel_info.publicChannel', defaultMessage: 'Public Channel'}) :
formatMessage({id: 'mobile.channel_info.privateChannel', defaultMessage: 'Private Channel'});
Alert.alert(
formatMessage(title, {term}),
formatMessage(
message,
{
term: term.toLowerCase(),
name: displayName,
},
),
[{
text: formatMessage({id: 'mobile.channel_info.alertNo', defaultMessage: 'No'}),
}, {
text: formatMessage({id: 'mobile.channel_info.alertYes', defaultMessage: 'Yes'}),
onPress: onPressAction,
}],
);
}
handleDelete = preventDoubleTap(() => {
const {channelId, deleteChannel, displayName, teamId} = this.props;
const title = {id: t('mobile.channel_info.alertTitleDeleteChannel'), defaultMessage: 'Archive {term}'};
const message = {
id: t('mobile.channel_info.alertMessageDeleteChannel'),
defaultMessage: 'Are you sure you want to archive the {term} {name}?',
};
const onPressAction = async () => {
const result = await deleteChannel(channelId);
if (result.error) {
alertErrorWithFallback(
this.context.intl,
result.error,
{
id: t('mobile.channel_info.delete_failed'),
defaultMessage: "We couldn't archive the channel {displayName}. Please check your connection and try again.",
},
{
displayName,
},
);
if (result.error.server_error_id === 'api.channel.delete_channel.deleted.app_error') {
this.props.getChannel(channelId);
}
} else if (this.props.viewArchivedChannels) {
this.props.handleSelectChannel(channelId);
this.props.close(false);
} else {
this.props.selectPenultimateChannel(teamId);
this.props.close(false);
}
};
this.alertAndHandleYesAction(title, message, onPressAction);
});
handleUnarchive = preventDoubleTap(() => {
const {channelId, displayName} = this.props;
const title = {id: t('mobile.channel_info.alertTitleUnarchiveChannel'), defaultMessage: 'Unarchive {term}'};
const message = {
id: t('mobile.channel_info.alertMessageUnarchiveChannel'),
defaultMessage: 'Are you sure you want to unarchive the {term} {name}?',
};
const onPressAction = async () => {
const result = await this.props.unarchiveChannel(channelId);
if (result.error) {
alertErrorWithFallback(
this.context.intl,
result.error,
{
id: t('mobile.channel_info.unarchive_failed'),
defaultMessage: "We couldn't unarchive the channel {displayName}. Please check your connection and try again.",
},
{
displayName,
},
);
if (result.error.server_error_id === 'api.channel.unarchive_channel.unarchive.app_error') {
this.props.getChannel(channelId);
}
} else {
this.props.close(false);
}
};
this.alertAndHandleYesAction(title, message, onPressAction);
});
render() {
const {canArchive, canUnarchive, isLandscape, theme} = this.props;
if (!canArchive && !canUnarchive) {
return null;
}
let element;
if (canUnarchive) {
element = (
<ChannelInfoRow
action={this.handleUnarchive}
defaultMessage='Unarchive Channel'
icon='archive'
iconColor='#CA3B27'
textColor='#CA3B27'
textId={t('mobile.routes.channelInfo.unarchive_channel')}
theme={theme}
isLandscape={isLandscape}
rightArrow={false}
/>
);
} else {
element = (
<ChannelInfoRow
action={this.handleDelete}
defaultMessage='Archive Channel'
iconColor='#CA3B27'
icon='archive'
textId={t('mobile.routes.channelInfo.delete_channel')}
textColor='#CA3B27'
theme={theme}
isLandscape={isLandscape}
rightArrow={false}
/>
);
}
return (
<>
<Separator theme={theme}/>
{element}
</>
);
}
}

View file

@ -0,0 +1,76 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {
handleSelectChannel,
selectPenultimateChannel,
} from '@actions/views/channel';
import {deleteChannel, getChannel, unarchiveChannel} from '@mm-redux/actions/channels';
import {General} from '@mm-redux/constants';
import Permissions from '@mm-redux/constants/permissions';
import {getCurrentChannel, isCurrentChannelReadOnly} from '@mm-redux/selectors/entities/channels';
import {getConfig, getLicense, hasNewPermissions} from '@mm-redux/selectors/entities/general';
import {haveITeamPermission} from '@mm-redux/selectors/entities/roles';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
import {getCurrentUser, getCurrentUserRoles} from '@mm-redux/selectors/entities/users';
import {showDeleteOption} from '@mm-redux/utils/channel_utils';
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
import {isAdmin as checkIsAdmin, isChannelAdmin as checkIsChannelAdmin, isSystemAdmin as checkIsSystemAdmin} from '@mm-redux/utils/user_utils';
import {isGuest as isUserGuest} from '@utils/users';
import Archive from './archive';
function mapStateToProps(state) {
const config = getConfig(state);
const license = getLicense(state);
const currentChannel = getCurrentChannel(state);
const currentUser = getCurrentUser(state);
const roles = getCurrentUserRoles(state) || '';
const isGuest = isUserGuest(currentUser);
const isDefaultChannel = currentChannel.name === General.DEFAULT_CHANNEL;
const isDirectMessage = currentChannel.type === General.DM_CHANNEL;
const isGroupMessage = currentChannel.type === General.GM_CHANNEL;
const isAdmin = checkIsAdmin(roles);
const isChannelAdmin = checkIsChannelAdmin(roles);
const isSystemAdmin = checkIsSystemAdmin(roles);
const canLeave = (!isDefaultChannel && !isDirectMessage && !isGroupMessage) || (isDefaultChannel && isGuest);
const canDelete = showDeleteOption(state, config, license, currentChannel, isAdmin, isSystemAdmin, isChannelAdmin);
const canUnarchive = (currentChannel?.delete_at > 0 && !isDirectMessage && !isGroupMessage);
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
const {serverVersion} = state.entities.general;
let isReadOnly = false;
if (currentUser?.id && currentChannel?.id) {
isReadOnly = isCurrentChannelReadOnly(state) || false;
}
let canUnarchiveChannel = false;
if (hasNewPermissions(state) && isMinimumServerVersion(serverVersion, 5, 20)) {
canUnarchiveChannel = haveITeamPermission(state, {
team: getCurrentTeamId(state),
permission: Permissions.MANAGE_TEAM,
});
}
return {
canArchive: (canLeave && canDelete && !isReadOnly),
canUnarchive: canUnarchive && canUnarchiveChannel,
channelId: currentChannel?.id || '',
displayName: (currentChannel?.display_name || '').trim(),
isPublic: currentChannel?.type === General.OPEN_CHANNEL,
teamId: currentChannel?.team_id || '',
viewArchivedChannels,
};
}
const mapDispatchToProps = {
deleteChannel,
getChannel,
handleSelectChannel,
unarchiveChannel,
selectPenultimateChannel,
};
export default connect(mapStateToProps, mapDispatchToProps)(Archive);

View file

@ -5,69 +5,47 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {
Alert,
ScrollView,
View,
} from 'react-native';
import {Navigation} from 'react-native-navigation';
import {dismissModal, goToScreen, showModalOverCurrentContext} from '@actions/navigation';
import pinIcon from '@assets/images/channel_info/pin.png';
import {dismissModal, showModalOverCurrentContext} from '@actions/navigation';
import StatusBar from '@components/status_bar';
import {General, Users} from '@mm-redux/constants';
import {alertErrorWithFallback} from '@utils/general';
import {t} from '@utils/i18n';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import AddMembers from './add_members';
import Archive from './archive';
import ChannelInfoHeader from './channel_info_header';
import ChannelInfoRow from './channel_info_row';
import ConvertPrivate from './convert_private';
import EditChannel from './edit_channel';
import Favorite from './favorite';
import IgnoreMentions from './ignore_mentions';
import Leave from './leave';
import ManageMembers from './manage_members';
import Mute from './mute';
import Pinned from './pinned';
import Separator from './separator';
export default class ChannelInfo extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
clearPinnedPosts: PropTypes.func.isRequired,
closeDMChannel: PropTypes.func.isRequired,
closeGMChannel: PropTypes.func.isRequired,
convertChannelToPrivate: PropTypes.func.isRequired,
deleteChannel: PropTypes.func.isRequired,
unarchiveChannel: PropTypes.func.isRequired,
getChannelStats: PropTypes.func.isRequired,
getChannel: PropTypes.func.isRequired,
leaveChannel: PropTypes.func.isRequired,
loadChannelsByTeamName: PropTypes.func.isRequired,
favoriteChannel: PropTypes.func.isRequired,
unfavoriteChannel: PropTypes.func.isRequired,
getCustomEmojisInText: PropTypes.func.isRequired,
selectFocusedPostId: PropTypes.func.isRequired,
updateChannelNotifyProps: PropTypes.func.isRequired,
selectPenultimateChannel: PropTypes.func.isRequired,
handleSelectChannel: PropTypes.func.isRequired,
setChannelDisplayName: PropTypes.func.isRequired,
}),
componentId: PropTypes.string,
viewArchivedChannels: PropTypes.bool.isRequired,
canDeleteChannel: PropTypes.bool.isRequired,
canUnarchiveChannel: PropTypes.bool.isRequired,
currentChannel: PropTypes.object.isRequired,
currentChannelCreatorName: PropTypes.string,
currentChannelMemberCount: PropTypes.number,
currentChannelGuestCount: PropTypes.number,
currentChannelPinnedPostCount: PropTypes.number,
currentChannelMemberCount: PropTypes.number,
currentUserId: PropTypes.string,
currentUserIsGuest: PropTypes.bool,
isBot: PropTypes.bool.isRequired,
isLandscape: PropTypes.bool.isRequired,
isTeammateGuest: PropTypes.bool.isRequired,
status: PropTypes.string,
theme: PropTypes.object.isRequired,
isChannelMuted: PropTypes.bool.isRequired,
isCurrent: PropTypes.bool.isRequired,
isFavorite: PropTypes.bool.isRequired,
canConvertChannel: PropTypes.bool.isRequired,
canManageUsers: PropTypes.bool.isRequired,
canEditChannel: PropTypes.bool.isRequired,
ignoreChannelMentions: PropTypes.bool.isRequired,
isBot: PropTypes.bool.isRequired,
isTeammateGuest: PropTypes.bool.isRequired,
isLandscape: PropTypes.bool.isRequired,
};
static defaultProps = {
@ -78,30 +56,6 @@ export default class ChannelInfo extends PureComponent {
intl: intlShape.isRequired,
};
constructor(props) {
super(props);
this.state = {
isFavorite: props.isFavorite,
isMuted: props.isChannelMuted,
ignoreChannelMentions: props.ignoreChannelMentions,
};
}
static getDerivedStateFromProps(nextProps, state) {
if (state.isFavorite !== nextProps.isFavorite ||
state.isMuted !== nextProps.isChannelMuted ||
state.ignoreChannelMentions !== nextProps.ignoreChannelMentions) {
return {
isFavorite: nextProps.isFavorite,
isMuted: nextProps.isChannelMuted,
ignoreChannelMentions: nextProps.ignoreChannelMentions,
};
}
return null;
}
componentDidMount() {
this.navigationEventListener = Navigation.events().bindComponent(this);
this.props.actions.getChannelStats(this.props.currentChannel.id);
@ -124,234 +78,6 @@ export default class ChannelInfo extends PureComponent {
dismissModal();
};
goToChannelAddMembers = preventDoubleTap(() => {
const {intl} = this.context;
const screen = 'ChannelAddMembers';
const title = intl.formatMessage({id: 'channel_header.addMembers', defaultMessage: 'Add Members'});
goToScreen(screen, title);
});
goToChannelMembers = preventDoubleTap(() => {
const {canManageUsers} = this.props;
const {intl} = this.context;
const id = canManageUsers ? t('channel_header.manageMembers') : t('channel_header.viewMembers');
const defaultMessage = canManageUsers ? 'Manage Members' : 'View Members';
const screen = 'ChannelMembers';
const title = intl.formatMessage({id, defaultMessage});
goToScreen(screen, title);
});
goToPinnedPosts = preventDoubleTap(() => {
const {currentChannel} = this.props;
const {formatMessage} = this.context.intl;
const id = t('channel_header.pinnedPosts');
const defaultMessage = 'Pinned Posts';
const screen = 'PinnedPosts';
const title = formatMessage({id, defaultMessage});
const passProps = {
currentChannelId: currentChannel.id,
};
goToScreen(screen, title, passProps);
});
handleChannelEdit = preventDoubleTap(() => {
const {intl} = this.context;
const id = t('mobile.channel_info.edit');
const defaultMessage = 'Edit Channel';
const screen = 'EditChannel';
const title = intl.formatMessage({id, defaultMessage});
goToScreen(screen, title);
});
handleConfirmConvertToPrivate = preventDoubleTap(async () => {
const {actions, currentChannel} = this.props;
const result = await actions.convertChannelToPrivate(currentChannel.id);
const displayName = {displayName: currentChannel.display_name.trim()};
const {formatMessage} = this.context.intl;
if (result.error) {
alertErrorWithFallback(
this.context.intl,
result.error,
{
id: t('mobile.channel_info.convert_failed'),
defaultMessage: 'We were unable to convert {displayName} to a private channel.',
},
{
displayName,
},
[{
text: formatMessage({id: 'mobile.share_extension.error_close', defaultMessage: 'Close'}),
}, {
text: formatMessage({id: 'mobile.terms_of_service.alert_retry', defaultMessage: 'Try Again'}),
onPress: this.handleConfirmConvertToPrivate,
}],
);
} else {
Alert.alert(
'',
formatMessage({id: t('mobile.channel_info.convert_success'), defaultMessage: '{displayName} is now a private channel.'}, displayName),
);
}
})
handleConvertToPrivate = preventDoubleTap(() => {
const {currentChannel} = this.props;
const {formatMessage} = this.context.intl;
const displayName = {displayName: currentChannel.display_name.trim()};
const title = {id: t('mobile.channel_info.alertTitleConvertChannel'), defaultMessage: 'Convert {displayName} to a private channel?'};
const message = {
id: t('mobile.channel_info.alertMessageConvertChannel'),
defaultMessage: 'When you convert {displayName} to a private channel, history and membership are preserved. Publicly shared files remain accessible to anyone with the link. Membership in a private channel is by invitation only.\n\nThe change is permanent and cannot be undone.\n\nAre you sure you want to convert {displayName} to a private channel?',
};
Alert.alert(
formatMessage(title, displayName),
formatMessage(message, displayName),
[{
text: formatMessage({id: 'mobile.channel_info.alertNo', defaultMessage: 'No'}),
}, {
text: formatMessage({id: 'mobile.channel_info.alertYes', defaultMessage: 'Yes'}),
onPress: this.handleConfirmConvertToPrivate,
}],
);
});
handleLeave = preventDoubleTap(() => {
const title = {id: t('mobile.channel_info.alertTitleLeaveChannel'), defaultMessage: 'Leave {term}'};
const message = {
id: t('mobile.channel_info.alertMessageLeaveChannel'),
defaultMessage: 'Are you sure you want to leave the {term} {name}?',
};
const onPressAction = () => {
this.props.actions.leaveChannel(this.props.currentChannel, true).then(() => {
this.close();
});
};
this.alertAndHandleYesAction(title, message, onPressAction);
});
handleDelete = preventDoubleTap(() => {
const channel = this.props.currentChannel;
const title = {id: t('mobile.channel_info.alertTitleDeleteChannel'), defaultMessage: 'Archive {term}'};
const message = {
id: t('mobile.channel_info.alertMessageDeleteChannel'),
defaultMessage: 'Are you sure you want to archive the {term} {name}?',
};
const onPressAction = async () => {
const result = await this.props.actions.deleteChannel(channel.id);
if (result.error) {
alertErrorWithFallback(
this.context.intl,
result.error,
{
id: t('mobile.channel_info.delete_failed'),
defaultMessage: "We couldn't archive the channel {displayName}. Please check your connection and try again.",
},
{
displayName: channel.display_name.trim(),
},
);
if (result.error.server_error_id === 'api.channel.delete_channel.deleted.app_error') {
this.props.actions.getChannel(channel.id);
}
} else if (this.props.viewArchivedChannels) {
this.props.actions.handleSelectChannel(channel.id);
this.close(false);
} else {
this.props.actions.selectPenultimateChannel(channel.team_id);
this.close(false);
}
};
this.alertAndHandleYesAction(title, message, onPressAction);
});
handleUnarchive = preventDoubleTap(() => {
const channel = this.props.currentChannel;
const title = {id: t('mobile.channel_info.alertTitleUnarchiveChannel'), defaultMessage: 'Unarchive {term}'};
const message = {
id: t('mobile.channel_info.alertMessageUnarchiveChannel'),
defaultMessage: 'Are you sure you want to unarchive the {term} {name}?',
};
const onPressAction = async () => {
const result = await this.props.actions.unarchiveChannel(channel.id);
if (result.error) {
alertErrorWithFallback(
this.context.intl,
result.error,
{
id: t('mobile.channel_info.unarchive_failed'),
defaultMessage: "We couldn't unarchive the channel {displayName}. Please check your connection and try again.",
},
{
displayName: channel.display_name.trim(),
},
);
if (result.error.server_error_id === 'api.channel.unarchive_channel.unarchive.app_error') {
this.props.actions.getChannel(channel.id);
}
} else {
this.close(false);
}
};
this.alertAndHandleYesAction(title, message, onPressAction);
});
alertAndHandleYesAction = (title, message, onPressAction) => {
const {formatMessage} = this.context.intl;
const channel = this.props.currentChannel;
const term = channel.type === General.OPEN_CHANNEL ?
formatMessage({id: 'mobile.channel_info.publicChannel', defaultMessage: 'Public Channel'}) :
formatMessage({id: 'mobile.channel_info.privateChannel', defaultMessage: 'Private Channel'});
Alert.alert(
formatMessage(title, {term}),
formatMessage(
message,
{
term: term.toLowerCase(),
name: channel.display_name.trim(),
},
),
[{
text: formatMessage({id: 'mobile.channel_info.alertNo', defaultMessage: 'No'}),
}, {
text: formatMessage({id: 'mobile.channel_info.alertYes', defaultMessage: 'Yes'}),
onPress: onPressAction,
}],
);
}
handleClose = preventDoubleTap(() => {
const {currentChannel, isCurrent, isFavorite} = this.props;
const channel = Object.assign({}, currentChannel, {isCurrent}, {isFavorite});
const {closeDMChannel, closeGMChannel} = this.props.actions;
switch (channel.type) {
case General.DM_CHANNEL:
closeDMChannel(channel).then(() => {
this.close();
});
break;
case General.GM_CHANNEL:
closeGMChannel(channel).then(() => {
this.close();
});
break;
}
});
handleFavorite = preventDoubleTap(() => {
const {isFavorite, actions, currentChannel} = this.props;
const {favoriteChannel, unfavoriteChannel} = actions;
const toggleFavorite = isFavorite ? unfavoriteChannel : favoriteChannel;
this.setState({isFavorite: !isFavorite});
toggleFavorite(currentChannel.id);
});
handleClosePermalink = () => {
const {actions} = this.props;
actions.selectFocusedPostId('');
@ -363,29 +89,6 @@ export default class ChannelInfo extends PureComponent {
this.showPermalinkView(postId);
};
handleMuteChannel = preventDoubleTap(() => {
const {actions, currentChannel, currentUserId, isChannelMuted} = this.props;
const {updateChannelNotifyProps} = actions;
const opts = {
mark_unread: isChannelMuted ? 'all' : 'mention',
};
this.setState({isMuted: !isChannelMuted});
updateChannelNotifyProps(currentUserId, currentChannel.id, opts);
});
handleIgnoreChannelMentions = preventDoubleTap(() => {
const {actions, currentChannel, currentUserId, ignoreChannelMentions} = this.props;
const {updateChannelNotifyProps} = actions;
const opts = {
ignore_channel_mentions: ignoreChannelMentions ? Users.IGNORE_CHANNEL_MENTIONS_OFF : Users.IGNORE_CHANNEL_MENTIONS_ON,
};
this.setState({ignoreChannelMentions: !ignoreChannelMentions});
updateChannelNotifyProps(currentUserId, currentChannel.id, opts);
});
showPermalinkView = (postId) => {
const {actions} = this.props;
const screen = 'Permalink';
@ -405,194 +108,66 @@ export default class ChannelInfo extends PureComponent {
showModalOverCurrentContext(screen, passProps, options);
};
renderViewOrManageMembersRow = () => {
const channel = this.props.currentChannel;
const isDirectMessage = channel.type === General.DM_CHANNEL;
return !isDirectMessage;
};
renderLeaveOrDeleteChannelRow = () => {
const channel = this.props.currentChannel;
const isGuest = this.props.currentUserIsGuest;
const isDefaultChannel = channel.name === General.DEFAULT_CHANNEL;
const isDirectMessage = channel.type === General.DM_CHANNEL;
const isGroupMessage = channel.type === General.GM_CHANNEL;
return (!isDefaultChannel && !isDirectMessage && !isGroupMessage) || (isDefaultChannel && isGuest);
};
renderCloseDirect = () => {
const channel = this.props.currentChannel;
const isDirectMessage = channel.type === General.DM_CHANNEL;
const isGroupMessage = channel.type === General.GM_CHANNEL;
return isDirectMessage || isGroupMessage;
};
renderUnarchiveChannel = () => {
const {canUnarchiveChannel} = this.props;
if (!canUnarchiveChannel) {
return false;
}
const channel = this.props.currentChannel;
const channelIsArchived = channel.delete_at !== 0;
const isDirectMessage = channel.type === General.DM_CHANNEL;
const isGroupMessage = channel.type === General.GM_CHANNEL;
return channelIsArchived && (!isDirectMessage && !isGroupMessage);
};
renderConvertToPrivateRow = () => {
const {currentChannel, canConvertChannel} = this.props;
const isDefaultChannel = currentChannel.name === General.DEFAULT_CHANNEL;
const isPublicChannel = currentChannel.type === General.OPEN_CHANNEL;
return !isDefaultChannel && isPublicChannel && canConvertChannel;
}
actionsRows = (style, channelIsArchived) => {
const {
currentChannelMemberCount,
currentChannelPinnedPostCount,
canManageUsers,
canEditChannel,
theme,
currentChannel,
isLandscape,
} = this.props;
const {currentChannel, currentUserId, isLandscape, theme} = this.props;
if (channelIsArchived) {
return (this.renderViewOrManageMembersRow() &&
<View>
<ChannelInfoRow
action={this.goToChannelMembers}
defaultMessage={canManageUsers ? 'Manage Members' : 'View Members'}
detail={currentChannelMemberCount}
icon='users'
textId={canManageUsers ? t('channel_header.manageMembers') : t('channel_header.viewMembers')}
theme={theme}
isLandscape={isLandscape}
/>
</View>);
return (
<ManageMembers
isLandscape={isLandscape}
theme={theme}
separator={false}
/>);
}
return (
<React.Fragment>
<ChannelInfoRow
action={this.handleFavorite}
defaultMessage='Favorite'
detail={this.state.isFavorite}
icon='star-o'
textId={t('mobile.routes.channelInfo.favorite')}
togglable={true}
theme={theme}
<>
<Favorite
channelId={currentChannel.id}
isLandscape={isLandscape}
/>
<View style={style.separator}/>
<ChannelInfoRow
action={this.handleMuteChannel}
defaultMessage='Mute channel'
detail={this.state.isMuted}
icon='bell-slash-o'
textId={t('channel_notifications.muteChannel.settings')}
togglable={true}
theme={theme}
isLandscape={isLandscape}
/>
<View style={style.separator}/>
<ChannelInfoRow
action={this.handleIgnoreChannelMentions}
defaultMessage='Ignore @channel, @here, @all'
detail={this.state.ignoreChannelMentions}
icon='at'
textId={t('channel_notifications.ignoreChannelMentions.settings')}
togglable={true}
<Separator theme={theme}/>
<Mute
channelId={currentChannel.id}
isLandscape={isLandscape}
userId={currentUserId}
theme={theme}
isLandscape={isLandscape}
/>
<View style={style.separator}/>
<ChannelInfoRow
action={this.goToPinnedPosts}
defaultMessage='Pinned Posts'
detail={currentChannelPinnedPostCount}
image={pinIcon}
textId={t('channel_header.pinnedPosts')}
<Separator theme={theme}/>
<IgnoreMentions
channelId={currentChannel.id}
isLandscape={isLandscape}
theme={theme}
isLandscape={isLandscape}
/>
{
/**
<ChannelInfoRow
action={() => true}
defaultMessage='Notification Preferences'
icon='bell-o'
textId='channel_header.notificationPreferences'
theme={theme}
/>
<View style={style.separator}/>
**/
}
{this.renderViewOrManageMembersRow() &&
<React.Fragment>
<View style={style.separator}/>
<ChannelInfoRow
action={this.goToChannelMembers}
defaultMessage={canManageUsers ? 'Manage Members' : 'View Members'}
detail={currentChannelMemberCount}
icon='users'
textId={canManageUsers ? t('channel_header.manageMembers') : t('channel_header.viewMembers')}
theme={theme}
isLandscape={isLandscape}
/>
</React.Fragment>
}
{canManageUsers && !currentChannel.group_constrained &&
<React.Fragment>
<View style={style.separator}/>
<ChannelInfoRow
action={this.goToChannelAddMembers}
defaultMessage='Add Members'
icon='user-plus'
textId={t('channel_header.addMembers')}
theme={theme}
isLandscape={isLandscape}
/>
</React.Fragment>
}
{this.renderConvertToPrivateRow() && (
<React.Fragment>
<View style={style.separator}/>
<ChannelInfoRow
action={this.handleConvertToPrivate}
defaultMessage='Convert to Private Channel'
icon='lock'
textId={t('mobile.channel_info.convert')}
theme={theme}
isLandscape={isLandscape}
/>
</React.Fragment>
)}
{canEditChannel && (
<React.Fragment>
<View style={style.separator}/>
<ChannelInfoRow
action={this.handleChannelEdit}
defaultMessage='Edit Channel'
icon='edit'
textId={t('mobile.channel_info.edit')}
theme={theme}
isLandscape={isLandscape}
/>
</React.Fragment>
)}
</React.Fragment>
<Separator theme={theme}/>
<Pinned
channelId={currentChannel.id}
isLandscape={isLandscape}
theme={theme}
/>
<ManageMembers
isLandscape={isLandscape}
theme={theme}
/>
<AddMembers
isLandscape={isLandscape}
theme={theme}
/>
<ConvertPrivate
isLandscape={isLandscape}
theme={theme}
/>
<EditChannel
isLandscape={isLandscape}
theme={theme}
/>
</>
);
};
render() {
const {
canDeleteChannel,
currentChannel,
currentChannelCreatorName,
currentChannelMemberCount,
@ -607,26 +182,13 @@ export default class ChannelInfo extends PureComponent {
const style = getStyleSheet(theme);
const channelIsArchived = currentChannel.delete_at !== 0;
let i18nId;
let defaultMessage;
switch (currentChannel.type) {
case General.DM_CHANNEL:
i18nId = t('mobile.channel_list.closeDM');
defaultMessage = 'Close Direct Message';
break;
case General.GM_CHANNEL:
i18nId = t('mobile.channel_list.closeGM');
defaultMessage = 'Close Group Message';
break;
}
return (
<View style={style.container}>
<StatusBar/>
<ScrollView
style={style.scrollView}
>
{currentChannel.hasOwnProperty('id') &&
{Boolean(currentChannel?.id) &&
<ChannelInfoHeader
createAt={currentChannel.create_at}
creator={currentChannelCreatorName}
@ -648,62 +210,19 @@ export default class ChannelInfo extends PureComponent {
}
<View style={style.rowsContainer}>
{this.actionsRows(style, channelIsArchived)}
{this.renderLeaveOrDeleteChannelRow() &&
<React.Fragment>
<View style={style.separator}/>
<ChannelInfoRow
action={this.handleLeave}
defaultMessage='Leave Channel'
icon='sign-out'
textId={t('navbar.leave')}
theme={theme}
isLandscape={isLandscape}
/>
{this.renderUnarchiveChannel() &&
<React.Fragment>
<View style={style.separator}/>
<ChannelInfoRow
action={this.handleUnarchive}
defaultMessage='Unarchive Channel'
icon='archive' // might need to change the icon...
textId={t('mobile.routes.channelInfo.unarchive_channel')}
theme={theme}
isLandscape={isLandscape}
/>
</React.Fragment>
}
</React.Fragment>
}
</View>
{this.renderLeaveOrDeleteChannelRow() && canDeleteChannel && !channelIsArchived &&
<View style={[style.rowsContainer, style.footer]}>
<ChannelInfoRow
action={this.handleDelete}
defaultMessage='Archive Channel'
iconColor='#CA3B27'
icon='archive'
textId={t('mobile.routes.channelInfo.delete_channel')}
textColor='#CA3B27'
theme={theme}
<View style={style.footer}>
<Leave
close={this.close}
isLandscape={isLandscape}
theme={theme}
/>
<Archive
close={this.close}
isLandscape={isLandscape}
theme={theme}
/>
</View>
}
{this.renderCloseDirect() &&
<View style={[style.rowsContainer, style.footer]}>
<ChannelInfoRow
action={this.handleClose}
defaultMessage={defaultMessage}
icon='times'
iconColor='#CA3B27'
rightArrow={false}
textId={i18nId}
textColor='#CA3B27'
theme={theme}
isLandscape={isLandscape}
/>
</View>
}
</ScrollView>
</View>
);
@ -721,10 +240,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
footer: {
marginTop: 40,
borderTopWidth: 1,
borderBottomWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
},
rowsContainer: {
borderTopWidth: 1,
@ -733,10 +248,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
backgroundColor: theme.centerChannelBg,
},
separator: {
marginHorizontal: 15,
height: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
},
};
});

View file

@ -9,11 +9,6 @@ import {General} from '@mm-redux/constants';
import ChannelInfo from './channel_info';
// ChannelInfoRow expects to receive the pinIcon as a number
jest.mock('@assets/images/channel_info/pin.png', () => {
return 1;
});
jest.mock('@utils/theme', () => {
const original = jest.requireActual('../../utils/theme');
return {
@ -22,7 +17,7 @@ jest.mock('@utils/theme', () => {
};
});
describe('channel_info', () => {
describe('channelInfo', () => {
const intlMock = {
formatMessage: jest.fn(),
formatDate: jest.fn(),
@ -34,12 +29,6 @@ describe('channel_info', () => {
now: jest.fn(),
};
const baseProps = {
canDeleteChannel: true,
canUnarchiveChannel: false,
canConvertChannel: true,
canManageUsers: true,
viewArchivedChannels: true,
canEditChannel: true,
currentChannel: {
id: '1234',
display_name: 'Channel Name',
@ -54,35 +43,17 @@ describe('channel_info', () => {
currentChannelMemberCount: 2,
currentChannelGuestCount: 0,
currentUserId: '1234',
currentUserIsGuest: false,
isChannelMuted: false,
ignoreChannelMentions: false,
isCurrent: true,
isFavorite: false,
status: 'status',
theme: Preferences.THEMES.default,
isBot: false,
isTeammateGuest: false,
isLandscape: false,
actions: {
clearPinnedPosts: jest.fn(),
closeDMChannel: jest.fn(),
closeGMChannel: jest.fn(),
convertChannelToPrivate: jest.fn(),
deleteChannel: jest.fn(),
unarchiveChannel: jest.fn(),
getChannelStats: jest.fn(),
getChannel: jest.fn(),
leaveChannel: jest.fn(),
loadChannelsByTeamName: jest.fn(),
favoriteChannel: jest.fn(),
unfavoriteChannel: jest.fn(),
getCustomEmojisInText: jest.fn(),
selectFocusedPostId: jest.fn(),
updateChannelNotifyProps: jest.fn(),
selectPenultimateChannel: jest.fn(),
setChannelDisplayName: jest.fn(),
handleSelectChannel: jest.fn(),
},
};
@ -96,63 +67,6 @@ describe('channel_info', () => {
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should render convert to private button when user has team admin permissions', async () => {
const wrapper = shallow(
<ChannelInfo
{...baseProps}
/>,
{context: {intl: intlMock}},
);
const instance = wrapper.instance();
const render = instance.renderConvertToPrivateRow();
expect(render).toBeTruthy();
});
test('should not render convert to private button when user is not team admin or above', async () => {
const wrapper = shallow(
<ChannelInfo
{...baseProps}
canConvertChannel={false}
/>,
{context: {intl: intlMock}},
);
const instance = wrapper.instance();
const render = instance.renderConvertToPrivateRow();
expect(render).toBeFalsy();
});
test('should not render convert to private button currentChannel is already private', async () => {
const props = Object.assign({}, baseProps);
props.currentChannel.type = General.PRIVATE_CHANNEL;
const wrapper = shallow(
<ChannelInfo
{...props}
/>,
{context: {intl: intlMock}},
);
const instance = wrapper.instance();
const render = instance.renderConvertToPrivateRow();
expect(render).toBeFalsy();
});
test('should not render convert to private button when currentChannel is a default channel', async () => {
const props = Object.assign({}, baseProps);
props.currentChannel.name = General.DEFAULT_CHANNEL;
const wrapper = shallow(
<ChannelInfo
{...props}
/>,
{context: {intl: intlMock}},
);
const instance = wrapper.instance();
const render = instance.renderConvertToPrivateRow();
expect(render).toBeFalsy();
});
test('should dismiss modal on close', () => {
const dismissModal = jest.spyOn(NavigationActions, 'dismissModal');
const wrapper = shallow(
@ -167,38 +81,4 @@ describe('channel_info', () => {
instance.close();
expect(dismissModal).toHaveBeenCalled();
});
test('should render unarchive channel button when currentChannel is an archived channel', async () => {
const props = Object.assign({}, baseProps);
props.canUnarchiveChannel = true;
props.currentChannel.delete_at = 1234566;
const wrapper = shallow(
<ChannelInfo
{...props}
/>,
{context: {intl: intlMock}},
);
const instance = wrapper.instance();
const render = instance.renderUnarchiveChannel();
expect(render).toBeTruthy();
});
test('should not render unarchive channel button when currentChannel is an active channel', async () => {
const props = Object.assign({}, baseProps);
props.canUnarchiveChannel = false;
props.currentChannel.delete_at = 0;
const wrapper = shallow(
<ChannelInfo
{...props}
/>,
{context: {intl: intlMock}},
);
const instance = wrapper.instance();
const render = instance.renderUnarchiveChannel();
expect(render).toBeFalsy();
});
});

View file

@ -52,7 +52,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
};
renderHasGuestText = (style) => {
const {type, hasGuests, isTeammateGuest} = this.props;
const {type, hasGuests, isLandscape, isTeammateGuest} = this.props;
if (!hasGuests) {
return null;
}
@ -74,7 +74,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
defaultMessage = 'This channel has guests';
}
return (
<View style={style.section}>
<View style={[style.section, padding(isLandscape, -15)]}>
<View style={style.row}>
<FormattedText
style={style.header}

View file

@ -102,6 +102,7 @@ channelInfoRow.propTypes = {
iconColor: PropTypes.string,
image: PropTypes.number,
imageTintColor: PropTypes.string,
isLandscape: PropTypes.bool,
rightArrow: PropTypes.bool,
textId: PropTypes.string.isRequired,
togglable: PropTypes.bool,

View file

@ -0,0 +1,76 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChannelInfo -> ConvertPrivate should match snapshot for Convert to Private Channel 1`] = `
<React.Fragment>
<Separator
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Convert to Private Channel"
icon="lock"
isLandscape={false}
rightArrow={false}
shouldRender={true}
textId="mobile.channel_info.convert"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={false}
/>
</React.Fragment>
`;

View file

@ -0,0 +1,47 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallowWithIntl} from 'test/intl-test-helper';
import Preferences from '@mm-redux/constants/preferences';
import ConvertPrivate from './convert_private';
jest.mock('@utils/theme', () => {
const original = jest.requireActual('../../../utils/theme');
return {
...original,
changeOpacity: jest.fn(),
};
});
describe('ChannelInfo -> ConvertPrivate', () => {
const baseProps = {
canConvert: true,
channelId: '123',
convertChannelToPrivate: jest.fn(),
displayName: 'Test Channel',
isLandscape: false,
theme: Preferences.THEMES.default,
};
test('should match snapshot for Convert to Private Channel', () => {
const wrapper = shallowWithIntl(
<ConvertPrivate
{...baseProps}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot Convert to Private Channel', () => {
const wrapper = shallowWithIntl(
<ConvertPrivate
{...baseProps}
canConvert={false}
/>,
);
expect(wrapper.getElement()).toBeNull();
});
});

View file

@ -0,0 +1,103 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {Alert} from 'react-native';
import {intlShape} from 'react-intl';
import {ActionResult} from '@mm-redux/types/actions';
import {Theme} from '@mm-redux/types/preferences';
import ChannelInfoRow from '@screens/channel_info/channel_info_row';
import Separator from '@screens/channel_info/separator';
import {alertErrorWithFallback} from '@utils/general';
import {t} from '@utils/i18n';
import {preventDoubleTap} from '@utils/tap';
interface ConvertPrivateProps {
canConvert: boolean;
channelId: string;
convertChannelToPrivate: (channelId: string) => Promise<ActionResult>;
displayName: string;
isLandscape: boolean;
theme: Theme;
}
export default class ConvertPrivate extends PureComponent<ConvertPrivateProps> {
static contextTypes = {
intl: intlShape.isRequired,
};
handleConfirmConvertToPrivate = preventDoubleTap(async () => {
const {channelId, convertChannelToPrivate, displayName} = this.props;
const result = await convertChannelToPrivate(channelId);
const {formatMessage} = this.context.intl;
if (result.error) {
alertErrorWithFallback(
this.context.intl,
result.error,
{
id: t('mobile.channel_info.convert_failed'),
defaultMessage: 'We were unable to convert {displayName} to a private channel.',
},
{
displayName,
},
[{
text: formatMessage({id: 'mobile.share_extension.error_close', defaultMessage: 'Close'}),
}, {
text: formatMessage({id: 'mobile.terms_of_service.alert_retry', defaultMessage: 'Try Again'}),
onPress: this.handleConfirmConvertToPrivate,
}],
);
} else {
Alert.alert(
'',
formatMessage({id: t('mobile.channel_info.convert_success'), defaultMessage: '{displayName} is now a private channel.'}, displayName),
);
}
});
handleConvertToPrivate = preventDoubleTap(() => {
const {displayName} = this.props;
const {formatMessage} = this.context.intl;
const title = {id: t('mobile.channel_info.alertTitleConvertChannel'), defaultMessage: 'Convert {displayName} to a private channel?'};
const message = {
id: t('mobile.channel_info.alertMessageConvertChannel'),
defaultMessage: 'When you convert {displayName} to a private channel, history and membership are preserved. Publicly shared files remain accessible to anyone with the link. Membership in a private channel is by invitation only.\n\nThe change is permanent and cannot be undone.\n\nAre you sure you want to convert {displayName} to a private channel?',
};
Alert.alert(
formatMessage(title, {displayName}),
formatMessage(message, {displayName}),
[{
text: formatMessage({id: 'mobile.channel_info.alertNo', defaultMessage: 'No'}),
}, {
text: formatMessage({id: 'mobile.channel_info.alertYes', defaultMessage: 'Yes'}),
onPress: this.handleConfirmConvertToPrivate,
}],
);
});
render() {
const {canConvert, isLandscape, theme} = this.props;
if (!canConvert) {
return null;
}
return (
<>
<Separator theme={theme}/>
<ChannelInfoRow
action={this.handleConvertToPrivate}
defaultMessage='Convert to Private Channel'
icon='lock'
isLandscape={isLandscape}
rightArrow={false}
theme={theme}
textId={t('mobile.channel_info.convert')}
/>
</>
);
}
}

View file

@ -0,0 +1,33 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {convertChannelToPrivate} from '@mm-redux/actions/channels';
import {General} from '@mm-redux/constants';
import {getCurrentChannel} from '@mm-redux/selectors/entities/channels';
import {getCurrentUserRoles} from '@mm-redux/selectors/entities/users';
import {isAdmin as checkIsAdmin} from '@mm-redux/utils/user_utils';
import ConvertPrivate from './convert_private';
function mapStateToProps(state) {
const currentChannel = getCurrentChannel(state);
const isDefaultChannel = currentChannel.name === General.DEFAULT_CHANNEL;
const isPublicChannel = currentChannel.type === General.OPEN_CHANNEL;
const roles = getCurrentUserRoles(state) || '';
const isAdmin = checkIsAdmin(roles);
const canConvert = !isDefaultChannel && isPublicChannel && isAdmin;
return {
canConvert,
channelId: currentChannel?.id || '',
displayName: (currentChannel?.display_name || '').trim(),
};
}
const mapDispatchToProps = {
convertChannelToPrivate,
};
export default connect(mapStateToProps, mapDispatchToProps)(ConvertPrivate);

View file

@ -0,0 +1,76 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChannelInfo -> EditChannel should match snapshot for Edit Channel 1`] = `
<React.Fragment>
<Separator
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Edit Channel"
icon="edit"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="mobile.channel_info.edit"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={false}
/>
</React.Fragment>
`;

View file

@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallowWithIntl} from 'test/intl-test-helper';
import Preferences from '@mm-redux/constants/preferences';
import EditChannel from './edit_channel';
jest.mock('@utils/theme', () => {
const original = jest.requireActual('../../../utils/theme');
return {
...original,
changeOpacity: jest.fn(),
};
});
describe('ChannelInfo -> EditChannel', () => {
const baseProps = {
canEdit: true,
isLandscape: false,
theme: Preferences.THEMES.default,
};
test('should match snapshot for Edit Channel', () => {
const wrapper = shallowWithIntl(
<EditChannel
{...baseProps}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot Not render EditChannel', () => {
const wrapper = shallowWithIntl(
<EditChannel
{...baseProps}
canEdit={false}
/>,
);
expect(wrapper.getElement()).toBeNull();
});
});

View file

@ -0,0 +1,56 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {intlShape} from 'react-intl';
import {goToScreen} from '@actions/navigation';
import {Theme} from '@mm-redux/types/preferences';
import ChannelInfoRow from '@screens/channel_info/channel_info_row';
import Separator from '@screens/channel_info/separator';
import {t} from '@utils/i18n';
import {preventDoubleTap} from '@utils/tap';
interface EditChannelProps {
canEdit: boolean;
isLandscape: boolean;
theme: Theme;
}
export default class EditChannel extends PureComponent<EditChannelProps> {
static contextTypes = {
intl: intlShape.isRequired,
};
handleChannelEdit = preventDoubleTap(() => {
const {intl} = this.context;
const id = t('mobile.channel_info.edit');
const defaultMessage = 'Edit Channel';
const screen = 'EditChannel';
const title = intl.formatMessage({id, defaultMessage});
goToScreen(screen, title);
});
render() {
const {canEdit, isLandscape, theme} = this.props;
if (!canEdit) {
return null;
}
return (
<>
<Separator theme={theme}/>
<ChannelInfoRow
action={this.handleChannelEdit}
defaultMessage='Edit Channel'
icon='edit'
textId={t('mobile.channel_info.edit')}
theme={theme}
isLandscape={isLandscape}
/>
</>
);
}
}

View file

@ -0,0 +1,36 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {getCurrentChannel, isCurrentChannelReadOnly} from '@mm-redux/selectors/entities/channels';
import {getConfig, getLicense} from '@mm-redux/selectors/entities/general';
import {getCurrentUserId, getCurrentUserRoles} from '@mm-redux/selectors/entities/users';
import {showManagementOptions} from '@mm-redux/utils/channel_utils';
import {isAdmin as checkIsAdmin, isChannelAdmin as checkIsChannelAdmin, isSystemAdmin as checkIsSystemAdmin} from '@mm-redux/utils/user_utils';
import EditChannel from './edit_channel';
function mapStateToProps(state) {
const config = getConfig(state);
const license = getLicense(state);
const currentChannel = getCurrentChannel(state);
const currentUserId = getCurrentUserId(state);
const roles = getCurrentUserRoles(state) || '';
const isAdmin = checkIsAdmin(roles);
const isChannelAdmin = checkIsChannelAdmin(roles);
const isSystemAdmin = checkIsSystemAdmin(roles);
let channelIsReadOnly = false;
if (currentUserId && currentChannel.id) {
channelIsReadOnly = isCurrentChannelReadOnly(state) || false;
}
const canEdit = !channelIsReadOnly && showManagementOptions(state, config, license, currentChannel, isAdmin, isSystemAdmin, isChannelAdmin);
return {
canEdit,
};
}
export default connect(mapStateToProps)(EditChannel);

View file

@ -0,0 +1,87 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChannelInfo -> Favorite should match snapshot for Favorite Channel 1`] = `
<channelInfoRow
action={[Function]}
defaultMessage="Favorite"
detail={true}
icon="star-o"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="mobile.routes.channelInfo.favorite"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={true}
/>
`;
exports[`ChannelInfo -> Favorite should match snapshot for Unfavorite Channel 1`] = `
<channelInfoRow
action={[Function]}
defaultMessage="Favorite"
detail={false}
icon="star-o"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="mobile.routes.channelInfo.favorite"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={true}
/>
`;

View file

@ -0,0 +1,47 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallowWithIntl} from 'test/intl-test-helper';
import Preferences from '@mm-redux/constants/preferences';
import Favorite from './favorite';
jest.mock('@utils/theme', () => {
const original = jest.requireActual('../../../utils/theme');
return {
...original,
changeOpacity: jest.fn(),
};
});
describe('ChannelInfo -> Favorite', () => {
const baseProps = {
channelId: '123',
favoriteChannel: jest.fn(),
isFavorite: false,
isLandscape: false,
unfavoriteChannel: jest.fn(),
theme: Preferences.THEMES.default,
};
test('should match snapshot for Favorite Channel', () => {
const wrapper = shallowWithIntl(
<Favorite
{...baseProps}
isFavorite={true}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for Unfavorite Channel', () => {
const wrapper = shallowWithIntl(
<Favorite
{...baseProps}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,43 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useState} from 'react';
import {Theme} from '@mm-redux/types/preferences';
import ChannelInfoRow from '@screens/channel_info/channel_info_row';
import {t} from '@utils/i18n';
import {preventDoubleTap} from '@utils/tap';
interface FavoriteProps {
channelId: string;
favoriteChannel: (channelId: string) => void;
isFavorite: boolean;
isLandscape: boolean;
unfavoriteChannel: (channelId: string) => void;
theme: Theme;
}
const Favorite = ({channelId, favoriteChannel, isLandscape, isFavorite, unfavoriteChannel, theme}: FavoriteProps) => {
const [favorite, setFavorite] = useState(isFavorite);
const handleFavorite = preventDoubleTap(() => {
const toggleFavorite = favorite ? unfavoriteChannel : favoriteChannel;
setFavorite(!favorite);
toggleFavorite(channelId);
}, 500);
return (
<ChannelInfoRow
action={handleFavorite}
defaultMessage='Favorite'
detail={favorite}
icon='star-o'
textId={t('mobile.routes.channelInfo.favorite')}
togglable={true}
theme={theme}
isLandscape={isLandscape}
/>
);
};
export default memo(Favorite);

View file

@ -0,0 +1,26 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {favoriteChannel, unfavoriteChannel} from '@mm-redux/actions/channels';
import {getFavoritesPreferences} from '@mm-redux/selectors/entities/preferences';
import Favorite from './favorite';
function mapStateToProps(state, ownProps) {
const {channelId} = ownProps;
const favoriteChannels = getFavoritesPreferences(state) || [];
return {
channelId,
isFavorite: favoriteChannels.indexOf(channelId) > -1,
};
}
const mapDispatchToProps = {
favoriteChannel,
unfavoriteChannel,
};
export default connect(mapStateToProps, mapDispatchToProps)(Favorite);

View file

@ -0,0 +1,87 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChannelInfo -> IgnoreMentions should match snapshot for Ignore Mentions 1`] = `
<channelInfoRow
action={[Function]}
defaultMessage="Ignore @channel, @here, @all"
detail={true}
icon="at"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="channel_notifications.ignoreChannelMentions.settings"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={true}
/>
`;
exports[`ChannelInfo -> IgnoreMentions should match snapshot for Not Ignore Mentions 1`] = `
<channelInfoRow
action={[Function]}
defaultMessage="Ignore @channel, @here, @all"
detail={false}
icon="at"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="channel_notifications.ignoreChannelMentions.settings"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={true}
/>
`;

View file

@ -0,0 +1,47 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallowWithIntl} from 'test/intl-test-helper';
import Preferences from '@mm-redux/constants/preferences';
import IgnoreMentions from './ignore_mentions';
jest.mock('@utils/theme', () => {
const original = jest.requireActual('../../../utils/theme');
return {
...original,
changeOpacity: jest.fn(),
};
});
describe('ChannelInfo -> IgnoreMentions', () => {
const baseProps = {
channelId: '123',
ignore: false,
isLandscape: false,
theme: Preferences.THEMES.default,
updateChannelNotifyProps: jest.fn(),
userId: 'user-123',
};
test('should match snapshot for Not Ignore Mentions', () => {
const wrapper = shallowWithIntl(
<IgnoreMentions
{...baseProps}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for Ignore Mentions', () => {
const wrapper = shallowWithIntl(
<IgnoreMentions
{...baseProps}
ignore={true}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,47 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useState} from 'react';
import {Users} from '@mm-redux/constants';
import {Theme} from '@mm-redux/types/preferences';
import ChannelInfoRow from '@screens/channel_info/channel_info_row';
import {t} from '@utils/i18n';
import {preventDoubleTap} from '@utils/tap';
interface IgnoreMentionsProps {
channelId: string;
ignore: boolean;
isLandscape: boolean;
theme: Theme;
updateChannelNotifyProps: (userId: string, channelId: string, opts: {ignore_channel_mentions: string}) => void;
userId: string;
}
const IgnoreMentions = ({channelId, ignore, isLandscape, updateChannelNotifyProps, userId, theme}: IgnoreMentionsProps) => {
const [mentions, setMentions] = useState(ignore);
const handleIgnoreChannelMentions = preventDoubleTap(() => {
const opts = {
ignore_channel_mentions: ignore ? Users.IGNORE_CHANNEL_MENTIONS_OFF : Users.IGNORE_CHANNEL_MENTIONS_ON,
};
setMentions(!mentions);
updateChannelNotifyProps(userId, channelId, opts);
});
return (
<ChannelInfoRow
action={handleIgnoreChannelMentions}
defaultMessage='Ignore @channel, @here, @all'
detail={ignore}
icon='at'
textId={t('channel_notifications.ignoreChannelMentions.settings')}
togglable={true}
theme={theme}
isLandscape={isLandscape}
/>
);
};
export default memo(IgnoreMentions);

View file

@ -0,0 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {updateChannelNotifyProps} from '@mm-redux/actions/channels';
import {getMyCurrentChannelMembership} from '@mm-redux/selectors/entities/channels';
import {getCurrentUser} from '@mm-redux/selectors/entities/users';
import {areChannelMentionsIgnored} from '@mm-redux/utils/channel_utils';
import IgnoreMentions from './ignore_mentions';
function mapStateToProps(state) {
const currentUser = getCurrentUser(state);
const currentChannelMember = getMyCurrentChannelMembership(state);
return {
ignore: areChannelMentionsIgnored(currentChannelMember && currentChannelMember.notify_props, currentUser.notify_props),
userId: currentUser?.id || '',
};
}
const mapDispatchToProps = {
updateChannelNotifyProps,
};
export default connect(mapStateToProps, mapDispatchToProps)(IgnoreMentions);

View file

@ -4,77 +4,30 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {
convertChannelToPrivate,
favoriteChannel,
getChannelStats,
getChannel,
deleteChannel,
unarchiveChannel,
unfavoriteChannel,
updateChannelNotifyProps,
} from '@mm-redux/actions/channels';
import {loadChannelsByTeamName, setChannelDisplayName} from '@actions/views/channel';
import {getChannelStats} from '@mm-redux/actions/channels';
import {getCustomEmojisInText} from '@mm-redux/actions/emojis';
import {selectFocusedPostId} from '@mm-redux/actions/posts';
import {clearPinnedPosts} from '@mm-redux/actions/search';
import {General} from '@mm-redux/constants';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {
canManageChannelMembers,
getCurrentChannel,
getCurrentChannelStats,
getSortedFavoriteChannelIds,
getMyCurrentChannelMembership,
isCurrentChannelReadOnly,
} from '@mm-redux/selectors/entities/channels';
import {getCurrentUserId, getUser, getStatusForUserId, getCurrentUserRoles} from '@mm-redux/selectors/entities/users';
import {areChannelMentionsIgnored, getUserIdFromChannelName, isChannelMuted, showDeleteOption, showManagementOptions} from '@mm-redux/utils/channel_utils';
import {isAdmin as checkIsAdmin, isChannelAdmin as checkIsChannelAdmin, isSystemAdmin as checkIsSystemAdmin} from '@mm-redux/utils/user_utils';
import {getConfig, getLicense, hasNewPermissions} from '@mm-redux/selectors/entities/general';
import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone';
import {getUserCurrentTimezone} from '@mm-redux/utils/timezone_utils';
import Permissions from '@mm-redux/constants/permissions';
import {haveITeamPermission} from '@mm-redux/selectors/entities/roles';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
import {
closeDMChannel,
closeGMChannel,
handleSelectChannel,
leaveChannel,
loadChannelsByTeamName,
selectPenultimateChannel,
setChannelDisplayName,
} from 'app/actions/views/channel';
import {isLandscape} from 'app/selectors/device';
import {isGuest} from 'app/utils/users';
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
import {getCurrentChannel, getCurrentChannelStats} from '@mm-redux/selectors/entities/channels';
import {getCurrentUserId, getUser, getStatusForUserId} from '@mm-redux/selectors/entities/users';
import {getUserIdFromChannelName} from '@mm-redux/utils/channel_utils';
import {displayUsername} from '@mm-redux/utils/user_utils';
import {isLandscape} from '@selectors/device';
import {isGuest} from '@utils/users';
import ChannelInfo from './channel_info';
function mapStateToProps(state) {
const config = getConfig(state);
const license = getLicense(state);
const currentChannel = getCurrentChannel(state) || {};
const currentChannelCreator = getUser(state, currentChannel.creator_id);
const currentChannelCreatorName = currentChannelCreator && currentChannelCreator.username;
const teammateNameDisplay = getTeammateNameDisplaySetting(state);
const currentChannelCreatorName = displayUsername(currentChannelCreator, teammateNameDisplay);
const currentChannelStats = getCurrentChannelStats(state);
const currentChannelMemberCount = currentChannelStats && currentChannelStats.member_count;
const currentChannelPinnedPostCount = currentChannelStats && currentChannelStats.pinnedpost_count;
let currentChannelGuestCount = (currentChannelStats && currentChannelStats.guest_count) || 0;
const currentChannelMember = getMyCurrentChannelMembership(state);
const currentUserId = getCurrentUserId(state);
const favoriteChannels = getSortedFavoriteChannelIds(state);
const isCurrent = currentChannel.id === state.entities.channels.currentChannelId;
const isFavorite = favoriteChannels && favoriteChannels.indexOf(currentChannel.id) > -1;
const roles = getCurrentUserRoles(state) || '';
const {serverVersion} = state.entities.general;
let canManageUsers = currentChannel.id ? canManageChannelMembers(state) : false;
if (currentChannel.group_constrained) {
canManageUsers = false;
}
const currentUser = getUser(state, currentUserId);
const currentUserIsGuest = isGuest(currentUser);
let status;
let isBot = false;
@ -92,80 +45,28 @@ function mapStateToProps(state) {
}
}
const isAdmin = checkIsAdmin(roles);
const isChannelAdmin = checkIsChannelAdmin(roles);
const isSystemAdmin = checkIsSystemAdmin(roles);
let channelIsReadOnly = false;
if (currentUserId && currentChannel.id) {
channelIsReadOnly = isCurrentChannelReadOnly(state) || false;
}
const canEditChannel = !channelIsReadOnly && showManagementOptions(state, config, license, currentChannel, isAdmin, isSystemAdmin, isChannelAdmin);
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
const enableTimezone = isTimezoneEnabled(state);
let timeZone = null;
if (enableTimezone) {
timeZone = getUserCurrentTimezone(currentUser.timezone);
}
let canUnarchiveChannel = false;
if (hasNewPermissions(state) && isMinimumServerVersion(serverVersion, 5, 20)) {
canUnarchiveChannel = haveITeamPermission(state, {
team: getCurrentTeamId(state),
permission: Permissions.MANAGE_TEAM,
});
}
return {
canDeleteChannel: showDeleteOption(state, config, license, currentChannel, isAdmin, isSystemAdmin, isChannelAdmin),
canUnarchiveChannel,
canConvertChannel: isAdmin,
viewArchivedChannels,
canEditChannel,
currentChannel,
currentChannelCreatorName,
currentChannelMemberCount,
currentChannelGuestCount,
currentChannelPinnedPostCount,
currentChannelMemberCount,
currentUserId,
currentUserIsGuest,
isChannelMuted: isChannelMuted(currentChannelMember),
ignoreChannelMentions: areChannelMentionsIgnored(currentChannelMember && currentChannelMember.notify_props, currentUser.notify_props),
isCurrent,
isFavorite,
isBot,
isLandscape: isLandscape(state),
isTeammateGuest,
status,
theme: getTheme(state),
canManageUsers,
isBot,
isTeammateGuest,
isLandscape: isLandscape(state),
timeZone,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
clearPinnedPosts,
closeDMChannel,
closeGMChannel,
convertChannelToPrivate,
deleteChannel,
unarchiveChannel,
getChannelStats,
getChannel,
leaveChannel,
loadChannelsByTeamName,
favoriteChannel,
unfavoriteChannel,
getCustomEmojisInText,
selectFocusedPostId,
updateChannelNotifyProps,
selectPenultimateChannel,
setChannelDisplayName,
handleSelectChannel,
}, dispatch),
};
}

View file

@ -0,0 +1,309 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChannelInfo -> Leave should match snapshot for Close DM 1`] = `
<React.Fragment>
<Separator
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Close Direct Message"
icon="times"
iconColor="#CA3B27"
isLandscape={false}
rightArrow={false}
shouldRender={true}
textColor="#CA3B27"
textId="mobile.channel_list.closeDM"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={false}
/>
</React.Fragment>
`;
exports[`ChannelInfo -> Leave should match snapshot for Close GM 1`] = `
<React.Fragment>
<Separator
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Close Group Message"
icon="times"
iconColor="#CA3B27"
isLandscape={false}
rightArrow={false}
shouldRender={true}
textColor="#CA3B27"
textId="mobile.channel_list.closeGM"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={false}
/>
</React.Fragment>
`;
exports[`ChannelInfo -> Leave should match snapshot for Leave Private Channel 1`] = `
<React.Fragment>
<Separator
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Leave Channel"
icon="sign-out"
iconColor="#CA3B27"
isLandscape={false}
rightArrow={false}
shouldRender={true}
textColor="#CA3B27"
textId="navbar.leave"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={false}
/>
</React.Fragment>
`;
exports[`ChannelInfo -> Leave should match snapshot for Leave Public Channel 1`] = `
<React.Fragment>
<Separator
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Leave Channel"
icon="sign-out"
iconColor="#CA3B27"
isLandscape={false}
rightArrow={false}
shouldRender={true}
textColor="#CA3B27"
textId="navbar.leave"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={false}
/>
</React.Fragment>
`;

View file

@ -0,0 +1,46 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {
closeDMChannel,
closeGMChannel,
leaveChannel,
} from '@actions/views/channel';
import {General} from '@mm-redux/constants';
import {getCurrentChannel} from '@mm-redux/selectors/entities/channels';
import {getFavoritesPreferences} from '@mm-redux/selectors/entities/preferences';
import {getCurrentUser} from '@mm-redux/selectors/entities/users';
import {isGuest as isUserGuest} from '@utils/users';
import Leave from './leave';
function mapStateToProps(state) {
const currentChannel = getCurrentChannel(state);
const currentUser = getCurrentUser(state);
const favoriteChannels = getFavoritesPreferences(state) || [];
const isGuest = isUserGuest(currentUser);
const isDefaultChannel = currentChannel.name === General.DEFAULT_CHANNEL;
const isDirectMessage = currentChannel.type === General.DM_CHANNEL;
const isGroupMessage = currentChannel.type === General.GM_CHANNEL;
const canLeave = (!isDefaultChannel && !isDirectMessage && !isGroupMessage) || (isDefaultChannel && isGuest);
return {
canLeave,
currentChannel,
displayName: (currentChannel?.display_name || '').trim(),
isDirectMessage,
isFavorite: favoriteChannels.indexOf(currentChannel?.id) > -1,
isGroupMessage,
isPublic: currentChannel?.type === General.OPEN_CHANNEL,
};
}
const mapDispatchToProps = {
closeDMChannel,
closeGMChannel,
leaveChannel,
};
export default connect(mapStateToProps, mapDispatchToProps)(Leave);

View file

@ -0,0 +1,98 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallowWithIntl} from 'test/intl-test-helper';
import {General} from '@mm-redux/constants';
import Preferences from '@mm-redux/constants/preferences';
import Leave from './leave';
jest.mock('@utils/theme', () => {
const original = jest.requireActual('../../../utils/theme');
return {
...original,
changeOpacity: jest.fn(),
};
});
describe('ChannelInfo -> Leave', () => {
const baseProps = {
canLeave: true,
currentChannel: {
id: '1234',
display_name: 'Channel Name',
type: General.OPEN_CHANNEL,
create_at: 123,
delete_at: 0,
header: '',
purpose: 'Purpose',
group_constrained: false,
},
close: jest.fn(),
closeDMChannel: jest.fn(),
closeGMChannel: jest.fn(),
displayName: 'Channel Name',
leaveChannel: jest.fn(),
isDirectMessage: false,
isFavorite: false,
isGroupMessage: false,
isLandscape: false,
isPublic: true,
theme: Preferences.THEMES.default,
};
test('should match snapshot for Leave Public Channel', () => {
const wrapper = shallowWithIntl(
<Leave
{...baseProps}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for Leave Private Channel', () => {
const wrapper = shallowWithIntl(
<Leave
{...baseProps}
isPublic={false}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for Close DM', () => {
const wrapper = shallowWithIntl(
<Leave
{...baseProps}
canLeave={false}
isDirectMessage={true}
isPublic={false}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for Close GM', () => {
const wrapper = shallowWithIntl(
<Leave
{...baseProps}
canLeave={false}
isGroupMessage={true}
isPublic={false}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should render null if cannot leave', () => {
const wrapper = shallowWithIntl(
<Leave
{...baseProps}
canLeave={false}
/>,
);
expect(wrapper.getElement()).toBeNull();
});
});

View file

@ -0,0 +1,149 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {Alert} from 'react-native';
import {intlShape} from 'react-intl';
import {ActionResult} from '@mm-redux/types/actions';
import {Channel} from '@mm-redux/types/channels';
import {FormattedMsg} from '@mm-redux/types/general';
import {Theme} from '@mm-redux/types/preferences';
import ChannelInfoRow from '@screens/channel_info/channel_info_row';
import Separator from '@screens/channel_info/separator';
import {t} from '@utils/i18n';
import {preventDoubleTap} from '@utils/tap';
interface LeaveProps {
canLeave: boolean;
currentChannel: Channel;
close: (redirect: boolean) => void;
closeDMChannel: (channel: Channel) => Promise<ActionResult>;
closeGMChannel: (channel: Channel) => Promise<ActionResult>;
displayName: string;
leaveChannel: (channel: Channel, reset: boolean) => Promise<ActionResult>;
isDirectMessage: boolean;
isFavorite: boolean;
isGroupMessage: boolean;
isLandscape: boolean;
isPublic: boolean;
theme: Theme;
}
export default class Leave extends PureComponent<LeaveProps> {
static contextTypes = {
intl: intlShape.isRequired,
};
alertAndHandleYesAction = (title: FormattedMsg, message: FormattedMsg, onPressAction: () => void) => {
const {formatMessage} = this.context.intl;
const {displayName, isPublic} = this.props;
// eslint-disable-next-line multiline-ternary
const term = isPublic ? formatMessage({id: 'mobile.channel_info.publicChannel', defaultMessage: 'Public Channel'}) :
formatMessage({id: 'mobile.channel_info.privateChannel', defaultMessage: 'Private Channel'});
Alert.alert(
formatMessage(title, {term}),
formatMessage(
message,
{
term: term.toLowerCase(),
name: displayName,
},
),
[{
text: formatMessage({id: 'mobile.channel_info.alertNo', defaultMessage: 'No'}),
}, {
text: formatMessage({id: 'mobile.channel_info.alertYes', defaultMessage: 'Yes'}),
onPress: onPressAction,
}],
);
}
handleClose = preventDoubleTap(() => {
const {close, currentChannel, isDirectMessage, isFavorite} = this.props;
const channel = Object.assign({}, currentChannel, {isCurrent: true}, {isFavorite});
const {closeDMChannel, closeGMChannel} = this.props;
if (isDirectMessage) {
closeDMChannel(channel).then(() => {
close(true);
});
} else {
closeGMChannel(channel).then(() => {
close(true);
});
}
});
handleLeave = preventDoubleTap(() => {
const title = {id: t('mobile.channel_info.alertTitleLeaveChannel'), defaultMessage: 'Leave {term}'};
const message = {
id: t('mobile.channel_info.alertMessageLeaveChannel'),
defaultMessage: 'Are you sure you want to leave the {term} {name}?',
};
const onPressAction = () => {
this.props.leaveChannel(this.props.currentChannel, true).then(() => {
this.props.close(true);
});
};
this.alertAndHandleYesAction(title, message, onPressAction);
});
render() {
const {canLeave, isDirectMessage, isGroupMessage, isLandscape, theme} = this.props;
if (!canLeave && !isDirectMessage && !isGroupMessage) {
return null;
}
let element;
if (isDirectMessage || isGroupMessage) {
let i18nId;
let defaultMessage;
if (isDirectMessage) {
i18nId = t('mobile.channel_list.closeDM');
defaultMessage = 'Close Direct Message';
} else {
i18nId = t('mobile.channel_list.closeGM');
defaultMessage = 'Close Group Message';
}
element = (
<ChannelInfoRow
action={this.handleClose}
defaultMessage={defaultMessage}
icon='times'
iconColor='#CA3B27'
textColor='#CA3B27'
textId={i18nId}
theme={theme}
isLandscape={isLandscape}
rightArrow={false}
/>
);
} else {
element = (
<ChannelInfoRow
action={this.handleLeave}
defaultMessage='Leave Channel'
icon='sign-out'
iconColor='#CA3B27'
textColor='#CA3B27'
textId={t('navbar.leave')}
theme={theme}
isLandscape={isLandscape}
rightArrow={false}
/>
);
}
return (
<>
<Separator theme={theme}/>
{element}
</>
);
}
}

View file

@ -0,0 +1,198 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChannelInfo -> Manage Members should match snapshot for Manage Members 1`] = `
<React.Fragment>
<Separator
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Manage Members"
detail={10}
icon="users"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="channel_header.manageMembers"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={false}
/>
</React.Fragment>
`;
exports[`ChannelInfo -> Manage Members should match snapshot for View Members 1`] = `
<React.Fragment>
<Separator
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="View Members"
detail={10}
icon="users"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="channel_header.viewMembers"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={false}
/>
</React.Fragment>
`;
exports[`ChannelInfo -> Manage Members should match snapshot without separator 1`] = `
<React.Fragment>
<channelInfoRow
action={[Function]}
defaultMessage="Manage Members"
detail={10}
icon="users"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="channel_header.manageMembers"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={false}
/>
</React.Fragment>
`;

View file

@ -0,0 +1,26 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {General} from '@mm-redux/constants';
import {canManageChannelMembers, getCurrentChannel, getCurrentChannelStats} from '@mm-redux/selectors/entities/channels';
import ManageMembers from './manage_members';
function mapStateToProps(state) {
const currentChannel = getCurrentChannel(state);
const currentChannelStats = getCurrentChannelStats(state);
const membersCount = currentChannelStats?.member_count || 0;
let canManageUsers = currentChannel?.id ? canManageChannelMembers(state) : false;
if (currentChannel.group_constrained) {
canManageUsers = false;
}
return {
canManageUsers,
isDirectMessage: currentChannel.type === General.DM_CHANNEL,
membersCount,
};
}
export default connect(mapStateToProps)(ManageMembers);

View file

@ -0,0 +1,67 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallowWithIntl} from 'test/intl-test-helper';
import Preferences from '@mm-redux/constants/preferences';
import ManageMembers from './manage_members';
jest.mock('@utils/theme', () => {
const original = jest.requireActual('../../../utils/theme');
return {
...original,
changeOpacity: jest.fn(),
};
});
describe('ChannelInfo -> Manage Members', () => {
const baseProps = {
canManageUsers: true,
isDirectMessage: false,
isLandscape: false,
membersCount: 10,
separator: true,
theme: Preferences.THEMES.default,
};
test('should match snapshot for Manage Members', () => {
const wrapper = shallowWithIntl(
<ManageMembers
{...baseProps}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot without separator', () => {
const wrapper = shallowWithIntl(
<ManageMembers
{...baseProps}
separator={false}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for View Members', () => {
const wrapper = shallowWithIntl(
<ManageMembers
{...baseProps}
canManageUsers={false}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should render null if channel is DM', () => {
const wrapper = shallowWithIntl(
<ManageMembers
{...baseProps}
isDirectMessage={true}
/>,
);
expect(wrapper.getElement()).toBeNull();
});
});

View file

@ -0,0 +1,65 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {intlShape} from 'react-intl';
import {goToScreen} from '@actions/navigation';
import {Theme} from '@mm-redux/types/preferences';
import ChannelInfoRow from '@screens/channel_info/channel_info_row';
import Separator from '@screens/channel_info/separator';
import {t} from '@utils/i18n';
import {preventDoubleTap} from '@utils/tap';
interface ManageMembersProps {
canManageUsers: boolean;
isDirectMessage: boolean;
isLandscape: boolean;
membersCount: number;
separator: boolean;
theme: Theme;
}
export default class ManageMembers extends PureComponent<ManageMembersProps> {
static contextTypes = {
intl: intlShape.isRequired,
};
static defaultProps = {
separator: true,
};
goToChannelMembers = preventDoubleTap(() => {
const {canManageUsers} = this.props;
const {intl} = this.context;
const id = canManageUsers ? t('channel_header.manageMembers') : t('channel_header.viewMembers');
const defaultMessage = canManageUsers ? 'Manage Members' : 'View Members';
const screen = 'ChannelMembers';
const title = intl.formatMessage({id, defaultMessage});
goToScreen(screen, title);
});
render() {
const {isDirectMessage, canManageUsers, isLandscape, membersCount, separator, theme} = this.props;
if (isDirectMessage) {
return null;
}
return (
<>
{separator && <Separator theme={theme}/>}
<ChannelInfoRow
action={this.goToChannelMembers}
defaultMessage={canManageUsers ? 'Manage Members' : 'View Members'}
detail={membersCount}
icon='users'
textId={canManageUsers ? t('channel_header.manageMembers') : t('channel_header.viewMembers')}
theme={theme}
isLandscape={isLandscape}
/>
</>
);
}
}

View file

@ -0,0 +1,87 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChannelInfo -> Mute should match snapshot for Muted 1`] = `
<channelInfoRow
action={[Function]}
defaultMessage="Mute channel"
detail={true}
icon="bell-slash-o"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="channel_notifications.muteChannel.settings"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={true}
/>
`;
exports[`ChannelInfo -> Mute should match snapshot for Not Muted 1`] = `
<channelInfoRow
action={[Function]}
defaultMessage="Mute channel"
detail={false}
icon="bell-slash-o"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="channel_notifications.muteChannel.settings"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={true}
/>
`;

View file

@ -0,0 +1,24 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {updateChannelNotifyProps} from '@mm-redux/actions/channels';
import {getMyCurrentChannelMembership} from '@mm-redux/selectors/entities/channels';
import {isChannelMuted} from '@mm-redux/utils/channel_utils';
import Mute from './mute';
function mapStateToProps(state) {
const currentChannelMember = getMyCurrentChannelMembership(state);
return {
isChannelMuted: isChannelMuted(currentChannelMember),
};
}
const mapDispatchToProps = {
updateChannelNotifyProps,
};
export default connect(mapStateToProps, mapDispatchToProps)(Mute);

View file

@ -0,0 +1,47 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallowWithIntl} from 'test/intl-test-helper';
import Preferences from '@mm-redux/constants/preferences';
import Mute from './mute';
jest.mock('@utils/theme', () => {
const original = jest.requireActual('../../../utils/theme');
return {
...original,
changeOpacity: jest.fn(),
};
});
describe('ChannelInfo -> Mute', () => {
const baseProps = {
channelId: '123',
isChannelMuted: false,
isLandscape: false,
theme: Preferences.THEMES.default,
updateChannelNotifyProps: jest.fn(),
userId: 'user-123',
};
test('should match snapshot for Not Muted', () => {
const wrapper = shallowWithIntl(
<Mute
{...baseProps}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for Muted', () => {
const wrapper = shallowWithIntl(
<Mute
{...baseProps}
isChannelMuted={true}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,46 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useState} from 'react';
import {Theme} from '@mm-redux/types/preferences';
import ChannelInfoRow from '@screens/channel_info/channel_info_row';
import {t} from '@utils/i18n';
import {preventDoubleTap} from '@utils/tap';
interface MuteProps {
channelId: string;
isChannelMuted: boolean;
isLandscape: boolean;
theme: Theme;
updateChannelNotifyProps: (userId: string, channelId: string, opts: {mark_unread: string}) => void;
userId: string;
}
const Mute = ({channelId, isChannelMuted, isLandscape, updateChannelNotifyProps, userId, theme}: MuteProps) => {
const [muted, setMuted] = useState(isChannelMuted);
const handleMuteChannel = preventDoubleTap(() => {
const opts = {
mark_unread: muted ? 'all' : 'mention',
};
setMuted(!muted);
updateChannelNotifyProps(userId, channelId, opts);
});
return (
<ChannelInfoRow
action={handleMuteChannel}
defaultMessage='Mute channel'
detail={muted}
icon='bell-slash-o'
textId={t('channel_notifications.muteChannel.settings')}
togglable={true}
theme={theme}
isLandscape={isLandscape}
/>
);
};
export default memo(Mute);

View file

@ -0,0 +1,44 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChannelInfo -> Pinned should match snapshot 1`] = `
<channelInfoRow
action={[Function]}
defaultMessage="Pinned Posts"
detail={0}
image={1}
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="channel_header.pinnedPosts"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
togglable={false}
/>
`;

View file

@ -0,0 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {getCurrentChannelStats} from '@mm-redux/selectors/entities/channels';
import Pinned from './pinned';
function mapStateToProps(state) {
const currentChannelStats = getCurrentChannelStats(state);
const pinnedCount = currentChannelStats?.pinnedpost_count || 0;
return {
pinnedCount,
};
}
export default connect(mapStateToProps)(Pinned);

View file

@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallowWithIntl} from 'test/intl-test-helper';
import Preferences from '@mm-redux/constants/preferences';
import Pinned from './pinned';
// Pinned expects to receive the pinIcon as a number
jest.mock('@assets/images/channel_info/pin.png', () => {
return 1;
});
jest.mock('@utils/theme', () => {
const original = jest.requireActual('../../../utils/theme');
return {
...original,
changeOpacity: jest.fn(),
};
});
describe('ChannelInfo -> Pinned', () => {
const baseProps = {
channelId: '123',
pinnedCount: 0,
isLandscape: false,
theme: Preferences.THEMES.default,
userId: 'user-123',
};
test('should match snapshot', () => {
const wrapper = shallowWithIntl(
<Pinned
{...baseProps}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,54 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {intlShape} from 'react-intl';
import {goToScreen} from '@actions/navigation';
import {Theme} from '@mm-redux/types/preferences';
import ChannelInfoRow from '@screens/channel_info/channel_info_row';
import {t} from '@utils/i18n';
import {preventDoubleTap} from '@utils/tap';
interface PinnedProps {
channelId: string;
pinnedCount: number;
isLandscape: boolean;
theme: Theme;
userId: string;
}
export default class Pinned extends PureComponent<PinnedProps> {
static contextTypes = {
intl: intlShape.isRequired,
};
goToPinnedPosts = preventDoubleTap(() => {
const {channelId} = this.props;
const {formatMessage} = this.context.intl;
const id = t('channel_header.pinnedPosts');
const defaultMessage = 'Pinned Posts';
const screen = 'PinnedPosts';
const title = formatMessage({id, defaultMessage});
const passProps = {
currentChannelId: channelId,
};
goToScreen(screen, title, passProps);
});
render() {
const {pinnedCount, isLandscape, theme} = this.props;
return (
<ChannelInfoRow
action={this.goToPinnedPosts}
defaultMessage='Pinned Posts'
detail={pinnedCount}
image={require('@assets/images/channel_info/pin.png')}
textId={t('channel_header.pinnedPosts')}
theme={theme}
isLandscape={isLandscape}
/>
);
}
}

View file

@ -0,0 +1,28 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {View} from 'react-native';
import {Theme} from '@mm-redux/types/preferences';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
interface SeparatorProps {
theme: Theme;
}
const Separator = ({theme}: SeparatorProps) => {
const style = getStyleSheet(theme);
return <View style={style.separator}/>;
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
separator: {
marginHorizontal: 15,
height: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
},
}));
export default Separator;

View file

@ -39,6 +39,7 @@
"@i18n": ["app/i18n/index"],
"@init/*": ["app/init/*"],
"@mm-redux/*": ["app/mm-redux/*"],
"@screens/*": ["app/screens/*"],
"@selectors/*": ["app/selectors/*"],
"@store/*": ["app/store"],
"@telemetry/*": ["/app/telemetry/*"],