diff --git a/app/components/autocomplete/autocomplete.js b/app/components/autocomplete/autocomplete.js index a2a8928fd..ab05feabe 100644 --- a/app/components/autocomplete/autocomplete.js +++ b/app/components/autocomplete/autocomplete.js @@ -149,7 +149,7 @@ export default class Autocomplete extends PureComponent { } else { // List is expanding downwards, likely from the search box let offset = Platform.select({ios: 65, android: 75}); - if (DeviceTypes.IS_IPHONE_X) { + if (DeviceTypes.IS_IPHONE_WITH_INSETS) { offset = 90; } diff --git a/app/components/client_upgrade_listener/client_upgrade_listener.js b/app/components/client_upgrade_listener/client_upgrade_listener.js index b05b3f14f..d7bb98eec 100644 --- a/app/components/client_upgrade_listener/client_upgrade_listener.js +++ b/app/components/client_upgrade_listener/client_upgrade_listener.js @@ -71,7 +71,7 @@ export default class ClientUpgradeListener extends PureComponent { if (versionMismatch && (forceUpgrade || Date.now() - lastUpgradeCheck > UPDATE_TIMEOUT)) { this.checkUpgrade(minVersion, latestVersion, nextProps.isLandscape); } else if (this.props.isLandscape !== nextProps.isLandscape && - isUpgradeAvailable(this.state.upgradeType) && DeviceTypes.IS_IPHONE_X) { + isUpgradeAvailable(this.state.upgradeType) && DeviceTypes.IS_IPHONE_WITH_INSETS) { const newTop = nextProps.isLandscape ? 45 : 100; this.setState({top: new Animated.Value(newTop)}); } @@ -98,10 +98,10 @@ export default class ClientUpgradeListener extends PureComponent { toggleUpgradeMessage = (show = true, isLandscape) => { let toValue = -100; if (show) { - if (DeviceTypes.IS_IPHONE_X && isLandscape) { + if (DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape) { toValue = 45; } else { - toValue = DeviceTypes.IS_IPHONE_X ? 100 : 75; + toValue = DeviceTypes.IS_IPHONE_WITH_INSETS ? 100 : 75; } } Animated.timing(this.state.top, { diff --git a/app/components/emoji_picker/emoji_picker.ios.js b/app/components/emoji_picker/emoji_picker.ios.js index c1603f6e7..4391679b6 100644 --- a/app/components/emoji_picker/emoji_picker.ios.js +++ b/app/components/emoji_picker/emoji_picker.ios.js @@ -27,7 +27,7 @@ export default class EmojiPicker extends EmojiPickerBase { const {emojis, filteredEmojis, searchTerm} = this.state; const styles = getStyleSheetFromTheme(theme); - const shorten = DeviceTypes.IS_IPHONE_X && isLandscape ? 6 : 2; + const shorten = DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape ? 6 : 2; let listComponent; if (searchTerm) { @@ -68,9 +68,9 @@ export default class EmojiPicker extends EmojiPickerBase { ); } - let keyboardOffset = DeviceTypes.IS_IPHONE_X ? 50 : 30; + let keyboardOffset = DeviceTypes.IS_IPHONE_WITH_INSETS ? 50 : 30; if (isLandscape) { - keyboardOffset = DeviceTypes.IS_IPHONE_X ? 0 : 10; + keyboardOffset = DeviceTypes.IS_IPHONE_WITH_INSETS ? 0 : 10; } const searchBarInput = { diff --git a/app/components/emoji_picker/emoji_picker_base.js b/app/components/emoji_picker/emoji_picker_base.js index 249df1ceb..9b496468e 100644 --- a/app/components/emoji_picker/emoji_picker_base.js +++ b/app/components/emoji_picker/emoji_picker_base.js @@ -216,7 +216,7 @@ export default class EmojiPicker extends PureComponent { }; getNumberOfColumns = (deviceWidth) => { - const shorten = DeviceTypes.IS_IPHONE_X && this.props.isLandscape ? 4 : 2; + const shorten = DeviceTypes.IS_IPHONE_WITH_INSETS && this.props.isLandscape ? 4 : 2; return Math.floor(Number(((deviceWidth - (SECTION_MARGIN * shorten)) / (EMOJI_SIZE + (EMOJI_GUTTER * shorten))))); }; diff --git a/app/components/network_indicator/network_indicator.js b/app/components/network_indicator/network_indicator.js index a29476fb2..0913735dc 100644 --- a/app/components/network_indicator/network_indicator.js +++ b/app/components/network_indicator/network_indicator.js @@ -201,11 +201,11 @@ export default class NetworkIndicator extends PureComponent { return ANDROID_TOP_PORTRAIT; } - const isX = DeviceTypes.IS_IPHONE_X; + const iPhoneWithInsets = DeviceTypes.IS_IPHONE_WITH_INSETS; - if (isX && isLandscape) { + if (iPhoneWithInsets && isLandscape) { return IOS_TOP_LANDSCAPE; - } else if (isX) { + } else if (iPhoneWithInsets) { return IOSX_TOP_PORTRAIT; } else if (isLandscape && !DeviceTypes.IS_TABLET) { return IOS_TOP_LANDSCAPE; diff --git a/app/components/safe_area_view/iphone_x_spacing/index.js b/app/components/safe_area_view/iphone_x_spacing/index.js index 9460b5e2e..8d8bfa9c6 100644 --- a/app/components/safe_area_view/iphone_x_spacing/index.js +++ b/app/components/safe_area_view/iphone_x_spacing/index.js @@ -4,17 +4,17 @@ import {DeviceTypes, ViewTypes} from 'app/constants'; export const paddingHorizontal = (isLandscape, modifier = 0) => { - return DeviceTypes.IS_IPHONE_X && isLandscape ? {paddingHorizontal: ViewTypes.IOS_HORIZONTAL_LANDSCAPE + modifier} : null; + return DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape ? {paddingHorizontal: ViewTypes.IOS_HORIZONTAL_LANDSCAPE + modifier} : null; }; export const paddingLeft = (isLandscape, modifier = 0) => { - return DeviceTypes.IS_IPHONE_X && isLandscape ? {paddingLeft: ViewTypes.IOS_HORIZONTAL_LANDSCAPE + modifier} : null; + return DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape ? {paddingLeft: ViewTypes.IOS_HORIZONTAL_LANDSCAPE + modifier} : null; }; export const paddingRight = (isLandscape, modifier = 0) => { - return DeviceTypes.IS_IPHONE_X && isLandscape ? {paddingRight: ViewTypes.IOS_HORIZONTAL_LANDSCAPE + modifier} : null; + return DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape ? {paddingRight: ViewTypes.IOS_HORIZONTAL_LANDSCAPE + modifier} : null; }; export const marginHorizontal = (isLandscape, modifier = 0) => { - return DeviceTypes.IS_IPHONE_X && isLandscape ? {marginHorizontal: ViewTypes.IOS_HORIZONTAL_LANDSCAPE + modifier} : null; -}; \ No newline at end of file + return DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape ? {marginHorizontal: ViewTypes.IOS_HORIZONTAL_LANDSCAPE + modifier} : null; +}; diff --git a/app/components/safe_area_view/safe_area_view.ios.js b/app/components/safe_area_view/safe_area_view.ios.js index 3f70d213b..97c5117ff 100644 --- a/app/components/safe_area_view/safe_area_view.ios.js +++ b/app/components/safe_area_view/safe_area_view.ios.js @@ -38,14 +38,14 @@ export default class SafeAreaIos extends PureComponent { super(props); let insetBottom = 0; - if ((DeviceTypes.IS_IPHONE_X || mattermostManaged.hasSafeAreaInsets) && props.excludeFooter) { + if ((DeviceTypes.IS_IPHONE_WITH_INSETS || mattermostManaged.hasSafeAreaInsets) && props.excludeFooter) { insetBottom = 20; } this.state = { keyboard: false, safeAreaInsets: { - top: DeviceTypes.IS_IPHONE_X ? 44 : 20, + top: DeviceTypes.IS_IPHONE_WITH_INSETS ? 44 : 20, left: 0, bottom: insetBottom, right: 0, @@ -94,7 +94,7 @@ export default class SafeAreaIos extends PureComponent { getSafeAreaInsets = () => { this.getStatusBarHeight(); - if (DeviceTypes.IS_IPHONE_X || mattermostManaged.hasSafeAreaInsets) { + if (DeviceTypes.IS_IPHONE_WITH_INSETS || mattermostManaged.hasSafeAreaInsets) { SafeArea.getSafeAreaInsetsForRootView().then((result) => { const {safeAreaInsets} = result; @@ -128,7 +128,7 @@ export default class SafeAreaIos extends PureComponent { } let top = safeAreaInsets.top; - if (forceTop && DeviceTypes.IS_IPHONE_X && !hideTopBar) { + if (forceTop && DeviceTypes.IS_IPHONE_WITH_INSETS && !hideTopBar) { top = forceTop; } diff --git a/app/components/sidebars/main/channels_list/list/list.js b/app/components/sidebars/main/channels_list/list/list.js index 691406d83..9b78a9e56 100644 --- a/app/components/sidebars/main/channels_list/list/list.js +++ b/app/components/sidebars/main/channels_list/list/list.js @@ -390,7 +390,7 @@ export default class List extends PureComponent { const {width, height} = Dimensions.get('window'); const landscape = width > height; - if (DeviceTypes.IS_IPHONE_X) { + if (DeviceTypes.IS_IPHONE_WITH_INSETS) { return landscape ? 54 : 44; } diff --git a/app/components/sidebars/main/main_sidebar.js b/app/components/sidebars/main/main_sidebar.js index 8ea04b391..84b5ef706 100644 --- a/app/components/sidebars/main/main_sidebar.js +++ b/app/components/sidebars/main/main_sidebar.js @@ -332,7 +332,7 @@ export default class ChannelSidebar extends Component { return null; } - const hasSafeAreaInsets = DeviceTypes.IS_IPHONE_X || mattermostManaged.hasSafeAreaInsets; + const hasSafeAreaInsets = DeviceTypes.IS_IPHONE_WITH_INSETS || mattermostManaged.hasSafeAreaInsets; const multipleTeams = teamsCount > 1; const showTeams = !searching && multipleTeams; if (this.drawerSwiper) { diff --git a/app/components/sidebars/main/teams_list/teams_list.js b/app/components/sidebars/main/teams_list/teams_list.js index afc927227..b139e7fa6 100644 --- a/app/components/sidebars/main/teams_list/teams_list.js +++ b/app/components/sidebars/main/teams_list/teams_list.js @@ -117,7 +117,7 @@ export default class TeamsList extends PureComponent { const {width, height} = Dimensions.get('window'); const landscape = width > height; - if (DeviceTypes.IS_IPHONE_X) { + if (DeviceTypes.IS_IPHONE_WITH_INSETS) { return landscape ? 54 : 44; } diff --git a/app/components/slide_up_panel/slide_up_panel.js b/app/components/slide_up_panel/slide_up_panel.js index dc15e2652..9c150243c 100644 --- a/app/components/slide_up_panel/slide_up_panel.js +++ b/app/components/slide_up_panel/slide_up_panel.js @@ -19,7 +19,7 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme'; import SlideUpPanelIndicator from './slide_up_panel_indicator'; export const BOTTOM_MARGIN = mattermostManaged.hasSafeAreaInsets ? 24 : 0; -const TOP_IOS_MARGIN = DeviceTypes.IS_IPHONE_X ? 84 : 64; +const TOP_IOS_MARGIN = DeviceTypes.IS_IPHONE_WITH_INSETS ? 84 : 64; const TOP_ANDROID_MARGIN = 44; const TOP_MARGIN = Platform.OS === 'ios' ? TOP_IOS_MARGIN : TOP_ANDROID_MARGIN; diff --git a/app/components/start/empty_toolbar.js b/app/components/start/empty_toolbar.js index 8db278b75..f3e5b399e 100644 --- a/app/components/start/empty_toolbar.js +++ b/app/components/start/empty_toolbar.js @@ -44,7 +44,7 @@ export default class EmptyToolbar extends PureComponent { height = IOS_TOP_LANDSCAPE; } - if (DeviceTypes.IS_IPHONE_X && isLandscape) { + if (DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape) { padding.paddingHorizontal = 10; } break; diff --git a/app/components/swiper.js b/app/components/swiper.js index 6e69bc0a8..22dbcdfb9 100644 --- a/app/components/swiper.js +++ b/app/components/swiper.js @@ -180,7 +180,7 @@ export default class Swiper extends PureComponent { const bottom = this.paginationBottom(width, height); const drawerWidth = (width > height) ? width - ViewTypes.IOS_HORIZONTAL_LANDSCAPE : width; let style; - if (DeviceTypes.IS_IPHONE_X && (width < height)) { + if (DeviceTypes.IS_IPHONE_WITH_INSETS && (width < height)) { style = { bottom, width: drawerWidth, @@ -235,7 +235,7 @@ export default class Swiper extends PureComponent { } const landscape = width > height; - if (DeviceTypes.IS_IPHONE_X) { + if (DeviceTypes.IS_IPHONE_WITH_INSETS) { return landscape ? 14 : 34; } diff --git a/app/constants/device.js b/app/constants/device.js index 71f56eefb..b71388c87 100644 --- a/app/constants/device.js +++ b/app/constants/device.js @@ -17,7 +17,7 @@ export default { ...deviceTypes, DOCUMENTS_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Documents`, IMAGES_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Images`, - IS_IPHONE_X: DeviceInfo.getModel().includes('iPhone X'), + IS_IPHONE_WITH_INSETS: DeviceInfo.getModel().includes('iPhone X') || DeviceInfo.getModel().includes('iPhone 11'), IS_TABLET: DeviceInfo.isTablet(), VIDEOS_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Videos`, PERMANENT_SIDEBAR_SETTINGS: '@PERMANENT_SIDEBAR_SETTINGS', diff --git a/app/screens/channel/channel.ios.js b/app/screens/channel/channel.ios.js index c7a9159a9..3319e7a92 100644 --- a/app/screens/channel/channel.ios.js +++ b/app/screens/channel/channel.ios.js @@ -39,7 +39,7 @@ export default class ChannelIOS extends ChannelBase { const {currentChannelId} = this.props; const channelLoaderStyle = [style.channelLoader, {height}]; - if ((DeviceTypes.IS_IPHONE_X || DeviceTypes.IS_TABLET)) { + if ((DeviceTypes.IS_IPHONE_WITH_INSETS || DeviceTypes.IS_TABLET)) { channelLoaderStyle.push(style.iOSHomeIndicator); } diff --git a/app/screens/channel/channel_nav_bar/channel_nav_bar.js b/app/screens/channel/channel_nav_bar/channel_nav_bar.js index 9949e6573..731d9e2ea 100644 --- a/app/screens/channel/channel_nav_bar/channel_nav_bar.js +++ b/app/screens/channel/channel_nav_bar/channel_nav_bar.js @@ -93,7 +93,7 @@ export default class ChannelNavBar extends PureComponent { canHaveSubtitle = false; } - if (DeviceTypes.IS_IPHONE_X && isLandscape) { + if (DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape) { canHaveSubtitle = false; } break; diff --git a/app/screens/channel_info/__snapshots__/channel_info.test.js.snap b/app/screens/channel_info/__snapshots__/channel_info.test.js.snap new file mode 100644 index 000000000..ea562ae80 --- /dev/null +++ b/app/screens/channel_info/__snapshots__/channel_info.test.js.snap @@ -0,0 +1,570 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`channel_info should match snapshot 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/app/screens/channel_info/channel_info.js b/app/screens/channel_info/channel_info.js index bd999dbb9..11b77a2f4 100644 --- a/app/screens/channel_info/channel_info.js +++ b/app/screens/channel_info/channel_info.js @@ -28,6 +28,7 @@ export default class ChannelInfo extends PureComponent { clearPinnedPosts: PropTypes.func.isRequired, closeDMChannel: PropTypes.func.isRequired, closeGMChannel: PropTypes.func.isRequired, + convertChannelToPrivate: PropTypes.func.isRequired, deleteChannel: PropTypes.func.isRequired, getChannelStats: PropTypes.func.isRequired, getChannel: PropTypes.func.isRequired, @@ -60,6 +61,7 @@ export default class ChannelInfo extends PureComponent { 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, @@ -176,6 +178,59 @@ export default class ChannelInfo extends PureComponent { this.handleDeleteOrLeave('delete'); }; + 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, + }], + ); + }); + handleDeleteOrLeave = preventDoubleTap((eventType) => { const {formatMessage} = this.context.intl; const channel = this.props.currentChannel; @@ -352,6 +407,13 @@ export default class ChannelInfo extends PureComponent { return 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, @@ -461,6 +523,19 @@ export default class ChannelInfo extends PureComponent { /> } + {this.renderConvertToPrivateRow() && ( + + + + + )} {canEditChannel && ( diff --git a/app/screens/channel_info/channel_info.test.js b/app/screens/channel_info/channel_info.test.js new file mode 100644 index 000000000..71d0ce1be --- /dev/null +++ b/app/screens/channel_info/channel_info.test.js @@ -0,0 +1,156 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React from 'react'; +import {shallow} from 'enzyme'; + +import Preferences from 'mattermost-redux/constants/preferences'; +import {General} from 'mattermost-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('app/utils/theme', () => { + const original = require.requireActual('app/utils/theme'); + return { + ...original, + changeOpacity: jest.fn(), + }; +}); + +describe('channel_info', () => { + const intlMock = { + formatMessage: jest.fn(), + formatDate: jest.fn(), + formatTime: jest.fn(), + formatRelative: jest.fn(), + formatNumber: jest.fn(), + formatPlural: jest.fn(), + formatHTMLMessage: jest.fn(), + now: jest.fn(), + }; + const baseProps = { + canDeleteChannel: true, + canConvertChannel: true, + canManageUsers: true, + viewArchivedChannels: true, + canEditChannel: true, + currentChannel: { + id: '1234', + display_name: 'Channel Name', + type: General.OPEN_CHANNEL, + create_at: 123, + delete_at: 0, + header: '', + purpose: 'Purpose', + group_constrained: false, + }, + currentChannelCreatorName: 'Creator', + currentChannelMemberCount: 2, + currentChannelGuestCount: 0, + currentUserId: '1234', + currentUserIsGuest: false, + isChannelMuted: false, + ignoreChannelMentions: false, + isCurrent: true, + isFavorite: false, + status: 'status', + theme: Preferences.THEMES.default, + isBot: false, + isLandscape: false, + actions: { + clearPinnedPosts: jest.fn(), + closeDMChannel: jest.fn(), + closeGMChannel: jest.fn(), + convertChannelToPrivate: jest.fn(), + deleteChannel: 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(), + popTopScreen: jest.fn(), + goToScreen: jest.fn(), + popToRoot: jest.fn(), + dismissModal: jest.fn(), + showModalOverCurrentContext: jest.fn(), + }, + }; + + test('should match snapshot', async () => { + const wrapper = shallow( + , + {context: {intl: intlMock}}, + ); + 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(); + }); +}); diff --git a/app/screens/channel_info/index.js b/app/screens/channel_info/index.js index 74a558da9..bbf55994a 100644 --- a/app/screens/channel_info/index.js +++ b/app/screens/channel_info/index.js @@ -5,6 +5,7 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import { + convertChannelToPrivate, favoriteChannel, getChannelStats, getChannel, @@ -105,6 +106,7 @@ function mapStateToProps(state) { return { canDeleteChannel: showDeleteOption(state, config, license, currentChannel, isAdmin, isSystemAdmin, isChannelAdmin), + canConvertChannel: isAdmin, viewArchivedChannels, canEditChannel, currentChannel, @@ -132,6 +134,7 @@ function mapDispatchToProps(dispatch) { clearPinnedPosts, closeDMChannel, closeGMChannel, + convertChannelToPrivate, deleteChannel, getChannelStats, getChannel, diff --git a/app/screens/image_preview/image_preview.js b/app/screens/image_preview/image_preview.js index e902a6b77..1d5d1a2e2 100644 --- a/app/screens/image_preview/image_preview.js +++ b/app/screens/image_preview/image_preview.js @@ -359,7 +359,7 @@ export default class ImagePreview extends PureComponent { const {deviceHeight, deviceWidth} = this.props; const {height, width} = imageDimensions; const {style, ...otherProps} = imageProps; - const statusBar = DeviceTypes.IS_IPHONE_X ? 0 : 20; + const statusBar = DeviceTypes.IS_IPHONE_WITH_INSETS ? 0 : 20; const flattenStyle = StyleSheet.flatten(style); const calculatedDimensions = calculateDimensions(height, width, deviceWidth, deviceHeight - statusBar); const imageStyle = {...flattenStyle, ...calculatedDimensions}; @@ -396,7 +396,7 @@ export default class ImagePreview extends PureComponent { renderVideoPreview = (file) => { const {deviceHeight, deviceWidth, theme} = this.props; - const statusBar = DeviceTypes.IS_IPHONE_X ? 0 : 20; + const statusBar = DeviceTypes.IS_IPHONE_WITH_INSETS ? 0 : 20; return ( diff --git a/app/utils/general.js b/app/utils/general.js index d8b962dfe..4352bec08 100644 --- a/app/utils/general.js +++ b/app/utils/general.js @@ -17,12 +17,12 @@ export function toTitleCase(str) { return str.replace(/\w\S*/g, doTitleCase); } -export function alertErrorWithFallback(intl, error, fallback, values) { +export function alertErrorWithFallback(intl, error, fallback, values, buttons) { let msg = error.message; if (!msg || msg === 'Network request failed') { msg = intl.formatMessage(fallback, values); } - Alert.alert('', msg); + Alert.alert('', msg, buttons); } export function alertErrorIfInvalidPermissions(result) { diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 92fce4088..b887fb92f 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -156,12 +156,17 @@ "mobile.camera_video_permission_denied_description": "Take videos and upload them to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your camera.", "mobile.camera_video_permission_denied_title": "{applicationName} would like to access your camera", "mobile.channel_drawer.search": "Jump to...", + "mobile.channel_info.alertMessageConvertChannel": "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?", "mobile.channel_info.alertMessageDeleteChannel": "Are you sure you want to archive the {term} {name}?", "mobile.channel_info.alertMessageLeaveChannel": "Are you sure you want to leave the {term} {name}?", "mobile.channel_info.alertNo": "No", + "mobile.channel_info.alertTitleConvertChannel": "Convert {displayName} to a private channel?", "mobile.channel_info.alertTitleDeleteChannel": "Archive {term}", "mobile.channel_info.alertTitleLeaveChannel": "Leave {term}", "mobile.channel_info.alertYes": "Yes", + "mobile.channel_info.convert": "Convert to Private Channel", + "mobile.channel_info.convert_failed": "We were unable to convert {displayName} to a private channel.", + "mobile.channel_info.convert_success": "{displayName} is now a private channel.", "mobile.channel_info.copy_header": "Copy Header", "mobile.channel_info.copy_purpose": "Copy Purpose", "mobile.channel_info.delete_failed": "We couldn't archive the channel {displayName}. Please check your connection and try again.", diff --git a/package.json b/package.json index f0229ffba..c1862ba56 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "react-native-calendars": "github:mattermost/react-native-calendars#4937ec5a3bf7e86f9f35fcd85eb4aa6133f45b58", "react-native-circular-progress": "1.1.0", "react-native-cookies": "github:joeferraro/react-native-cookies#f11374745deba9f18f7b8a9bb4b0b2573026f522", - "react-native-device-info": "github:mattermost/react-native-device-info#66d730b4f675038867ca389be7374714c2db63b2", + "react-native-device-info": "github:mattermost/react-native-device-info#a434e5378a59825b4c33c9a31bf0d8dc98a45966", "react-native-doc-viewer": "2.7.8", "react-native-document-picker": "2.3.0", "react-native-exception-handler": "2.10.7",