From 55511e1fdbaa10c8309ba686a91dabb34463bb83 Mon Sep 17 00:00:00 2001 From: Mattermost Build Date: Wed, 19 Aug 2020 10:18:46 -0400 Subject: [PATCH] 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 55cfce9b8958637d3180da88283361412e4b5b64) Co-authored-by: Elias Nahum --- app/mm-redux/types/actions.ts | 7 +- app/mm-redux/types/general.ts | 5 + app/mm-redux/types/module.d.ts | 3 +- .../__snapshots__/channel_info.test.js.snap | 551 +++++++-------- .../channel_info_header.test.js.snap | 27 +- .../__snapshots__/add_members.test.js.snap | 76 +++ .../add_members/add_members.test.js | 55 ++ .../channel_info/add_members/add_members.tsx | 55 ++ app/screens/channel_info/add_members/index.js | 22 + .../__snapshots__/archive.test.js.snap | 155 +++++ .../channel_info/archive/archive.test.js | 67 ++ app/screens/channel_info/archive/archive.tsx | 177 +++++ app/screens/channel_info/archive/index.js | 76 +++ app/screens/channel_info/channel_info.js | 626 ++---------------- app/screens/channel_info/channel_info.test.js | 122 +--- .../channel_info/channel_info_header.js | 4 +- app/screens/channel_info/channel_info_row.js | 1 + .../convert_private.test.js.snap | 76 +++ .../convert_private/convert_private.test.js | 47 ++ .../convert_private/convert_private.tsx | 103 +++ .../channel_info/convert_private/index.js | 33 + .../__snapshots__/edit_channel.test.js.snap | 76 +++ .../edit_channel/edit_channel.test.js | 44 ++ .../edit_channel/edit_channel.tsx | 56 ++ .../channel_info/edit_channel/index.js | 36 + .../__snapshots__/favorite.test.js.snap | 87 +++ .../channel_info/favorite/favorite.test.js | 47 ++ .../channel_info/favorite/favorite.tsx | 43 ++ app/screens/channel_info/favorite/index.js | 26 + .../ignore_mentions.test.js.snap | 87 +++ .../ignore_mentions/ignore_mentions.test.js | 47 ++ .../ignore_mentions/ignore_mentions.tsx | 47 ++ .../channel_info/ignore_mentions/index.js | 27 + app/screens/channel_info/index.js | 129 +--- .../leave/__snapshots__/leave.test.js.snap | 309 +++++++++ app/screens/channel_info/leave/index.js | 46 ++ app/screens/channel_info/leave/leave.test.js | 98 +++ app/screens/channel_info/leave/leave.tsx | 149 +++++ .../__snapshots__/manage_members.test.js.snap | 198 ++++++ .../channel_info/manage_members/index.js | 26 + .../manage_members/manage_members.test.js | 67 ++ .../manage_members/manage_members.tsx | 65 ++ .../mute/__snapshots__/mute.test.js.snap | 87 +++ app/screens/channel_info/mute/index.js | 24 + app/screens/channel_info/mute/mute.test.js | 47 ++ app/screens/channel_info/mute/mute.tsx | 46 ++ .../pinned/__snapshots__/pinned.test.js.snap | 44 ++ app/screens/channel_info/pinned/index.js | 19 + .../channel_info/pinned/pinned.test.js | 41 ++ app/screens/channel_info/pinned/pinned.tsx | 54 ++ app/screens/channel_info/separator.tsx | 28 + tsconfig.json | 1 + 52 files changed, 3269 insertions(+), 1120 deletions(-) create mode 100644 app/screens/channel_info/add_members/__snapshots__/add_members.test.js.snap create mode 100644 app/screens/channel_info/add_members/add_members.test.js create mode 100644 app/screens/channel_info/add_members/add_members.tsx create mode 100644 app/screens/channel_info/add_members/index.js create mode 100644 app/screens/channel_info/archive/__snapshots__/archive.test.js.snap create mode 100644 app/screens/channel_info/archive/archive.test.js create mode 100644 app/screens/channel_info/archive/archive.tsx create mode 100644 app/screens/channel_info/archive/index.js create mode 100644 app/screens/channel_info/convert_private/__snapshots__/convert_private.test.js.snap create mode 100644 app/screens/channel_info/convert_private/convert_private.test.js create mode 100644 app/screens/channel_info/convert_private/convert_private.tsx create mode 100644 app/screens/channel_info/convert_private/index.js create mode 100644 app/screens/channel_info/edit_channel/__snapshots__/edit_channel.test.js.snap create mode 100644 app/screens/channel_info/edit_channel/edit_channel.test.js create mode 100644 app/screens/channel_info/edit_channel/edit_channel.tsx create mode 100644 app/screens/channel_info/edit_channel/index.js create mode 100644 app/screens/channel_info/favorite/__snapshots__/favorite.test.js.snap create mode 100644 app/screens/channel_info/favorite/favorite.test.js create mode 100644 app/screens/channel_info/favorite/favorite.tsx create mode 100644 app/screens/channel_info/favorite/index.js create mode 100644 app/screens/channel_info/ignore_mentions/__snapshots__/ignore_mentions.test.js.snap create mode 100644 app/screens/channel_info/ignore_mentions/ignore_mentions.test.js create mode 100644 app/screens/channel_info/ignore_mentions/ignore_mentions.tsx create mode 100644 app/screens/channel_info/ignore_mentions/index.js create mode 100644 app/screens/channel_info/leave/__snapshots__/leave.test.js.snap create mode 100644 app/screens/channel_info/leave/index.js create mode 100644 app/screens/channel_info/leave/leave.test.js create mode 100644 app/screens/channel_info/leave/leave.tsx create mode 100644 app/screens/channel_info/manage_members/__snapshots__/manage_members.test.js.snap create mode 100644 app/screens/channel_info/manage_members/index.js create mode 100644 app/screens/channel_info/manage_members/manage_members.test.js create mode 100644 app/screens/channel_info/manage_members/manage_members.tsx create mode 100644 app/screens/channel_info/mute/__snapshots__/mute.test.js.snap create mode 100644 app/screens/channel_info/mute/index.js create mode 100644 app/screens/channel_info/mute/mute.test.js create mode 100644 app/screens/channel_info/mute/mute.tsx create mode 100644 app/screens/channel_info/pinned/__snapshots__/pinned.test.js.snap create mode 100644 app/screens/channel_info/pinned/index.js create mode 100644 app/screens/channel_info/pinned/pinned.test.js create mode 100644 app/screens/channel_info/pinned/pinned.tsx create mode 100644 app/screens/channel_info/separator.tsx diff --git a/app/mm-redux/types/actions.ts b/app/mm-redux/types/actions.ts index 29b1c3c8d..eeca2d61f 100644 --- a/app/mm-redux/types/actions.ts +++ b/app/mm-redux/types/actions.ts @@ -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; export type ActionFunc = (dispatch: DispatchFunc, getState: GetStateFunc) => Promise | ActionResult; diff --git a/app/mm-redux/types/general.ts b/app/mm-redux/types/general.ts index c51e0b7de..c2554141c 100644 --- a/app/mm-redux/types/general.ts +++ b/app/mm-redux/types/general.ts @@ -13,3 +13,8 @@ export type GeneralState = { serverVersion: string; timezones: Array; }; + +export type FormattedMsg = { + id: string; + defaultMessage: string; +}; diff --git a/app/mm-redux/types/module.d.ts b/app/mm-redux/types/module.d.ts index 345b4f3ab..e37f27d29 100644 --- a/app/mm-redux/types/module.d.ts +++ b/app/mm-redux/types/module.d.ts @@ -2,4 +2,5 @@ // See LICENSE.txt for license information. declare module 'gfycat-sdk'; declare module 'remote-redux-devtools'; -declare module 'redux-action-buffer'; \ No newline at end of file +declare module 'redux-action-buffer'; +declare module 'react-intl'; \ No newline at end of file diff --git a/app/screens/channel_info/__snapshots__/channel_info.test.js.snap b/app/screens/channel_info/__snapshots__/channel_info.test.js.snap index 48d9d6d3e..899c6c738 100644 --- a/app/screens/channel_info/__snapshots__/channel_info.test.js.snap +++ b/app/screens/channel_info/__snapshots__/channel_info.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`channel_info should match snapshot 1`] = ` +exports[`channelInfo should match snapshot 1`] = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - + diff --git a/app/screens/channel_info/__snapshots__/channel_info_header.test.js.snap b/app/screens/channel_info/__snapshots__/channel_info_header.test.js.snap index 4adeea687..08716d8ea 100644 --- a/app/screens/channel_info/__snapshots__/channel_info_header.test.js.snap +++ b/app/screens/channel_info/__snapshots__/channel_info_header.test.js.snap @@ -395,9 +395,12 @@ exports[`channel_info_header should match snapshot when DM and hasGuests and is Add Members should match snapshot 1`] = ` + + + + +`; diff --git a/app/screens/channel_info/add_members/add_members.test.js b/app/screens/channel_info/add_members/add_members.test.js new file mode 100644 index 000000000..fd8ce4ef9 --- /dev/null +++ b/app/screens/channel_info/add_members/add_members.test.js @@ -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( + , + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + test('should render null if cannot manage members', () => { + const wrapper = shallowWithIntl( + , + ); + expect(wrapper.getElement()).toBeNull(); + }); + + test('should render null if channel is constrained to groups', () => { + const wrapper = shallowWithIntl( + , + ); + expect(wrapper.getElement()).toBeNull(); + }); +}); \ No newline at end of file diff --git a/app/screens/channel_info/add_members/add_members.tsx b/app/screens/channel_info/add_members/add_members.tsx new file mode 100644 index 000000000..aa6e72e95 --- /dev/null +++ b/app/screens/channel_info/add_members/add_members.tsx @@ -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 { + 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 ( + <> + + + + ); + } + + return null; + } +} diff --git a/app/screens/channel_info/add_members/index.js b/app/screens/channel_info/add_members/index.js new file mode 100644 index 000000000..5b40e5022 --- /dev/null +++ b/app/screens/channel_info/add_members/index.js @@ -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); diff --git a/app/screens/channel_info/archive/__snapshots__/archive.test.js.snap b/app/screens/channel_info/archive/__snapshots__/archive.test.js.snap new file mode 100644 index 000000000..b0592f770 --- /dev/null +++ b/app/screens/channel_info/archive/__snapshots__/archive.test.js.snap @@ -0,0 +1,155 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ChannelInfo -> Archive should match snapshot for Archive Channel 1`] = ` + + + + +`; + +exports[`ChannelInfo -> Archive should match snapshot for Unarchive Channel 1`] = ` + + + + +`; diff --git a/app/screens/channel_info/archive/archive.test.js b/app/screens/channel_info/archive/archive.test.js new file mode 100644 index 000000000..43fd4b3df --- /dev/null +++ b/app/screens/channel_info/archive/archive.test.js @@ -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( + , + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + test('should match snapshot for Unarchive Channel', () => { + const wrapper = shallowWithIntl( + , + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + test('should match snapshot Not render Archive', () => { + const wrapper = shallowWithIntl( + , + ); + expect(wrapper.getElement()).toBeNull(); + }); +}); \ No newline at end of file diff --git a/app/screens/channel_info/archive/archive.tsx b/app/screens/channel_info/archive/archive.tsx new file mode 100644 index 000000000..298dd8333 --- /dev/null +++ b/app/screens/channel_info/archive/archive.tsx @@ -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; + displayName: string; + getChannel: (channelId: string) => Promise; + handleSelectChannel: (channelId: string) => Promise; + isLandscape: boolean; + isPublic: boolean; + unarchiveChannel: (channelId: string) => Promise; + selectPenultimateChannel: (channelId: string) => Promise; + teamId: string; + theme: Theme; + viewArchivedChannels: boolean; +} + +export default class Archive extends PureComponent { + 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 = ( + + ); + } else { + element = ( + + ); + } + + return ( + <> + + {element} + + ); + } +} diff --git a/app/screens/channel_info/archive/index.js b/app/screens/channel_info/archive/index.js new file mode 100644 index 000000000..8df586c14 --- /dev/null +++ b/app/screens/channel_info/archive/index.js @@ -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); diff --git a/app/screens/channel_info/channel_info.js b/app/screens/channel_info/channel_info.js index 9829799e9..b5fbe54dd 100644 --- a/app/screens/channel_info/channel_info.js +++ b/app/screens/channel_info/channel_info.js @@ -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() && - - - ); + return ( + ); } return ( - - + - - - - + - - + - { - - /** - true} - defaultMessage='Notification Preferences' - icon='bell-o' - textId='channel_header.notificationPreferences' - theme={theme} - /> - - **/ - } - {this.renderViewOrManageMembersRow() && - - - - - } - {canManageUsers && !currentChannel.group_constrained && - - - - - } - {this.renderConvertToPrivateRow() && ( - - - - - )} - {canEditChannel && ( - - - - - )} - + + + + + + + ); }; 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 ( - {currentChannel.hasOwnProperty('id') && + {Boolean(currentChannel?.id) && {this.actionsRows(style, channelIsArchived)} - {this.renderLeaveOrDeleteChannelRow() && - - - - {this.renderUnarchiveChannel() && - - - - - } - - } - {this.renderLeaveOrDeleteChannelRow() && canDeleteChannel && !channelIsArchived && - - + + - } - {this.renderCloseDirect() && - - - - } ); @@ -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), - }, }; }); diff --git a/app/screens/channel_info/channel_info.test.js b/app/screens/channel_info/channel_info.test.js index 50799426f..bfbbfc68a 100644 --- a/app/screens/channel_info/channel_info.test.js +++ b/app/screens/channel_info/channel_info.test.js @@ -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( - , - {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( - , - {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( - , - {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( - , - {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( - , - {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( - , - {context: {intl: intlMock}}, - ); - - const instance = wrapper.instance(); - const render = instance.renderUnarchiveChannel(); - expect(render).toBeFalsy(); - }); }); diff --git a/app/screens/channel_info/channel_info_header.js b/app/screens/channel_info/channel_info_header.js index 74fde359f..da21bae13 100644 --- a/app/screens/channel_info/channel_info_header.js +++ b/app/screens/channel_info/channel_info_header.js @@ -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 ( - + ConvertPrivate should match snapshot for Convert to Private Channel 1`] = ` + + + + +`; diff --git a/app/screens/channel_info/convert_private/convert_private.test.js b/app/screens/channel_info/convert_private/convert_private.test.js new file mode 100644 index 000000000..7576d5c0c --- /dev/null +++ b/app/screens/channel_info/convert_private/convert_private.test.js @@ -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( + , + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + test('should match snapshot Convert to Private Channel', () => { + const wrapper = shallowWithIntl( + , + ); + expect(wrapper.getElement()).toBeNull(); + }); +}); \ No newline at end of file diff --git a/app/screens/channel_info/convert_private/convert_private.tsx b/app/screens/channel_info/convert_private/convert_private.tsx new file mode 100644 index 000000000..8c86f09f1 --- /dev/null +++ b/app/screens/channel_info/convert_private/convert_private.tsx @@ -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; + displayName: string; + isLandscape: boolean; + theme: Theme; +} + +export default class ConvertPrivate extends PureComponent { + 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 ( + <> + + + + ); + } +} diff --git a/app/screens/channel_info/convert_private/index.js b/app/screens/channel_info/convert_private/index.js new file mode 100644 index 000000000..fd67b4e35 --- /dev/null +++ b/app/screens/channel_info/convert_private/index.js @@ -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); diff --git a/app/screens/channel_info/edit_channel/__snapshots__/edit_channel.test.js.snap b/app/screens/channel_info/edit_channel/__snapshots__/edit_channel.test.js.snap new file mode 100644 index 000000000..9c43c1a27 --- /dev/null +++ b/app/screens/channel_info/edit_channel/__snapshots__/edit_channel.test.js.snap @@ -0,0 +1,76 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ChannelInfo -> EditChannel should match snapshot for Edit Channel 1`] = ` + + + + +`; diff --git a/app/screens/channel_info/edit_channel/edit_channel.test.js b/app/screens/channel_info/edit_channel/edit_channel.test.js new file mode 100644 index 000000000..11bf35a53 --- /dev/null +++ b/app/screens/channel_info/edit_channel/edit_channel.test.js @@ -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( + , + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + test('should match snapshot Not render EditChannel', () => { + const wrapper = shallowWithIntl( + , + ); + expect(wrapper.getElement()).toBeNull(); + }); +}); \ No newline at end of file diff --git a/app/screens/channel_info/edit_channel/edit_channel.tsx b/app/screens/channel_info/edit_channel/edit_channel.tsx new file mode 100644 index 000000000..bcadce0b4 --- /dev/null +++ b/app/screens/channel_info/edit_channel/edit_channel.tsx @@ -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 { + 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 ( + <> + + + + ); + } +} diff --git a/app/screens/channel_info/edit_channel/index.js b/app/screens/channel_info/edit_channel/index.js new file mode 100644 index 000000000..ea9722727 --- /dev/null +++ b/app/screens/channel_info/edit_channel/index.js @@ -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); diff --git a/app/screens/channel_info/favorite/__snapshots__/favorite.test.js.snap b/app/screens/channel_info/favorite/__snapshots__/favorite.test.js.snap new file mode 100644 index 000000000..3894b306e --- /dev/null +++ b/app/screens/channel_info/favorite/__snapshots__/favorite.test.js.snap @@ -0,0 +1,87 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ChannelInfo -> Favorite should match snapshot for Favorite Channel 1`] = ` + +`; + +exports[`ChannelInfo -> Favorite should match snapshot for Unfavorite Channel 1`] = ` + +`; diff --git a/app/screens/channel_info/favorite/favorite.test.js b/app/screens/channel_info/favorite/favorite.test.js new file mode 100644 index 000000000..973f3d56f --- /dev/null +++ b/app/screens/channel_info/favorite/favorite.test.js @@ -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( + , + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + test('should match snapshot for Unfavorite Channel', () => { + const wrapper = shallowWithIntl( + , + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/app/screens/channel_info/favorite/favorite.tsx b/app/screens/channel_info/favorite/favorite.tsx new file mode 100644 index 000000000..6a6c52b49 --- /dev/null +++ b/app/screens/channel_info/favorite/favorite.tsx @@ -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 ( + + ); +}; + +export default memo(Favorite); \ No newline at end of file diff --git a/app/screens/channel_info/favorite/index.js b/app/screens/channel_info/favorite/index.js new file mode 100644 index 000000000..35714ebf0 --- /dev/null +++ b/app/screens/channel_info/favorite/index.js @@ -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); diff --git a/app/screens/channel_info/ignore_mentions/__snapshots__/ignore_mentions.test.js.snap b/app/screens/channel_info/ignore_mentions/__snapshots__/ignore_mentions.test.js.snap new file mode 100644 index 000000000..4387aa1b3 --- /dev/null +++ b/app/screens/channel_info/ignore_mentions/__snapshots__/ignore_mentions.test.js.snap @@ -0,0 +1,87 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ChannelInfo -> IgnoreMentions should match snapshot for Ignore Mentions 1`] = ` + +`; + +exports[`ChannelInfo -> IgnoreMentions should match snapshot for Not Ignore Mentions 1`] = ` + +`; diff --git a/app/screens/channel_info/ignore_mentions/ignore_mentions.test.js b/app/screens/channel_info/ignore_mentions/ignore_mentions.test.js new file mode 100644 index 000000000..85dcf6d1f --- /dev/null +++ b/app/screens/channel_info/ignore_mentions/ignore_mentions.test.js @@ -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( + , + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + test('should match snapshot for Ignore Mentions', () => { + const wrapper = shallowWithIntl( + , + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/app/screens/channel_info/ignore_mentions/ignore_mentions.tsx b/app/screens/channel_info/ignore_mentions/ignore_mentions.tsx new file mode 100644 index 000000000..42dd1545b --- /dev/null +++ b/app/screens/channel_info/ignore_mentions/ignore_mentions.tsx @@ -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 ( + + ); +}; + +export default memo(IgnoreMentions); \ No newline at end of file diff --git a/app/screens/channel_info/ignore_mentions/index.js b/app/screens/channel_info/ignore_mentions/index.js new file mode 100644 index 000000000..28ba109a4 --- /dev/null +++ b/app/screens/channel_info/ignore_mentions/index.js @@ -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); diff --git a/app/screens/channel_info/index.js b/app/screens/channel_info/index.js index 3c1ecd75e..3defab5bf 100644 --- a/app/screens/channel_info/index.js +++ b/app/screens/channel_info/index.js @@ -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), }; } diff --git a/app/screens/channel_info/leave/__snapshots__/leave.test.js.snap b/app/screens/channel_info/leave/__snapshots__/leave.test.js.snap new file mode 100644 index 000000000..5c1bdd213 --- /dev/null +++ b/app/screens/channel_info/leave/__snapshots__/leave.test.js.snap @@ -0,0 +1,309 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ChannelInfo -> Leave should match snapshot for Close DM 1`] = ` + + + + +`; + +exports[`ChannelInfo -> Leave should match snapshot for Close GM 1`] = ` + + + + +`; + +exports[`ChannelInfo -> Leave should match snapshot for Leave Private Channel 1`] = ` + + + + +`; + +exports[`ChannelInfo -> Leave should match snapshot for Leave Public Channel 1`] = ` + + + + +`; diff --git a/app/screens/channel_info/leave/index.js b/app/screens/channel_info/leave/index.js new file mode 100644 index 000000000..4c1ca17f9 --- /dev/null +++ b/app/screens/channel_info/leave/index.js @@ -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); diff --git a/app/screens/channel_info/leave/leave.test.js b/app/screens/channel_info/leave/leave.test.js new file mode 100644 index 000000000..44ba80d26 --- /dev/null +++ b/app/screens/channel_info/leave/leave.test.js @@ -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( + , + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + test('should match snapshot for Leave Private Channel', () => { + const wrapper = shallowWithIntl( + , + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + test('should match snapshot for Close DM', () => { + const wrapper = shallowWithIntl( + , + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + test('should match snapshot for Close GM', () => { + const wrapper = shallowWithIntl( + , + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + test('should render null if cannot leave', () => { + const wrapper = shallowWithIntl( + , + ); + expect(wrapper.getElement()).toBeNull(); + }); +}); \ No newline at end of file diff --git a/app/screens/channel_info/leave/leave.tsx b/app/screens/channel_info/leave/leave.tsx new file mode 100644 index 000000000..e7e5fdafe --- /dev/null +++ b/app/screens/channel_info/leave/leave.tsx @@ -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; + closeGMChannel: (channel: Channel) => Promise; + displayName: string; + leaveChannel: (channel: Channel, reset: boolean) => Promise; + isDirectMessage: boolean; + isFavorite: boolean; + isGroupMessage: boolean; + isLandscape: boolean; + isPublic: boolean; + theme: Theme; +} + +export default class Leave extends PureComponent { + 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 = ( + + ); + } else { + element = ( + + ); + } + + return ( + <> + + {element} + + ); + } +} diff --git a/app/screens/channel_info/manage_members/__snapshots__/manage_members.test.js.snap b/app/screens/channel_info/manage_members/__snapshots__/manage_members.test.js.snap new file mode 100644 index 000000000..0a2e03a3b --- /dev/null +++ b/app/screens/channel_info/manage_members/__snapshots__/manage_members.test.js.snap @@ -0,0 +1,198 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ChannelInfo -> Manage Members should match snapshot for Manage Members 1`] = ` + + + + +`; + +exports[`ChannelInfo -> Manage Members should match snapshot for View Members 1`] = ` + + + + +`; + +exports[`ChannelInfo -> Manage Members should match snapshot without separator 1`] = ` + + + +`; diff --git a/app/screens/channel_info/manage_members/index.js b/app/screens/channel_info/manage_members/index.js new file mode 100644 index 000000000..f5e53c8bd --- /dev/null +++ b/app/screens/channel_info/manage_members/index.js @@ -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); diff --git a/app/screens/channel_info/manage_members/manage_members.test.js b/app/screens/channel_info/manage_members/manage_members.test.js new file mode 100644 index 000000000..cf11d3858 --- /dev/null +++ b/app/screens/channel_info/manage_members/manage_members.test.js @@ -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( + , + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + test('should match snapshot without separator', () => { + const wrapper = shallowWithIntl( + , + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + test('should match snapshot for View Members', () => { + const wrapper = shallowWithIntl( + , + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + test('should render null if channel is DM', () => { + const wrapper = shallowWithIntl( + , + ); + expect(wrapper.getElement()).toBeNull(); + }); +}); \ No newline at end of file diff --git a/app/screens/channel_info/manage_members/manage_members.tsx b/app/screens/channel_info/manage_members/manage_members.tsx new file mode 100644 index 000000000..729ef6cb1 --- /dev/null +++ b/app/screens/channel_info/manage_members/manage_members.tsx @@ -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 { + 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 && } + + + ); + } +} diff --git a/app/screens/channel_info/mute/__snapshots__/mute.test.js.snap b/app/screens/channel_info/mute/__snapshots__/mute.test.js.snap new file mode 100644 index 000000000..bb06f2fe4 --- /dev/null +++ b/app/screens/channel_info/mute/__snapshots__/mute.test.js.snap @@ -0,0 +1,87 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ChannelInfo -> Mute should match snapshot for Muted 1`] = ` + +`; + +exports[`ChannelInfo -> Mute should match snapshot for Not Muted 1`] = ` + +`; diff --git a/app/screens/channel_info/mute/index.js b/app/screens/channel_info/mute/index.js new file mode 100644 index 000000000..c6eeb19ad --- /dev/null +++ b/app/screens/channel_info/mute/index.js @@ -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); diff --git a/app/screens/channel_info/mute/mute.test.js b/app/screens/channel_info/mute/mute.test.js new file mode 100644 index 000000000..6bea784ad --- /dev/null +++ b/app/screens/channel_info/mute/mute.test.js @@ -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( + , + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + test('should match snapshot for Muted', () => { + const wrapper = shallowWithIntl( + , + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/app/screens/channel_info/mute/mute.tsx b/app/screens/channel_info/mute/mute.tsx new file mode 100644 index 000000000..ea07da2fb --- /dev/null +++ b/app/screens/channel_info/mute/mute.tsx @@ -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 ( + + ); +}; + +export default memo(Mute); \ No newline at end of file diff --git a/app/screens/channel_info/pinned/__snapshots__/pinned.test.js.snap b/app/screens/channel_info/pinned/__snapshots__/pinned.test.js.snap new file mode 100644 index 000000000..f3be11a30 --- /dev/null +++ b/app/screens/channel_info/pinned/__snapshots__/pinned.test.js.snap @@ -0,0 +1,44 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ChannelInfo -> Pinned should match snapshot 1`] = ` + +`; diff --git a/app/screens/channel_info/pinned/index.js b/app/screens/channel_info/pinned/index.js new file mode 100644 index 000000000..67cab0624 --- /dev/null +++ b/app/screens/channel_info/pinned/index.js @@ -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); diff --git a/app/screens/channel_info/pinned/pinned.test.js b/app/screens/channel_info/pinned/pinned.test.js new file mode 100644 index 000000000..f630df3a3 --- /dev/null +++ b/app/screens/channel_info/pinned/pinned.test.js @@ -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( + , + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/app/screens/channel_info/pinned/pinned.tsx b/app/screens/channel_info/pinned/pinned.tsx new file mode 100644 index 000000000..d3a558796 --- /dev/null +++ b/app/screens/channel_info/pinned/pinned.tsx @@ -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 { + 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 ( + + ); + } +} diff --git a/app/screens/channel_info/separator.tsx b/app/screens/channel_info/separator.tsx new file mode 100644 index 000000000..65a62c5db --- /dev/null +++ b/app/screens/channel_info/separator.tsx @@ -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 ; +}; + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ + separator: { + marginHorizontal: 15, + height: 1, + backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), + }, +})); + +export default Separator; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 3a4cd9a29..91c3373a9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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/*"],