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",