MM-36721 [GEKIDOU] Porting Channel Nav Bar (#5550)

* MM_36721 : Added testscript for AppVersion component + Corrected imported Type definition

* Added CompassIcon component

* Adding TextProps to FormattedText component

* Added status bar component

* Added User status component

* Added ProfilePicture, did_update hook and sorted imports

* Added ChannelIcon component

* Added ChannelTitle component

* Added Channel Nav Bar component

* Added channel screen

* Added withSafeAreaInsets HOC and Added font compassIcon to Xcode

* Fix Android crashes as it is looking for MainSidebar and SettingsSidebar

* Revert "Fix Android crashes as it is looking for MainSidebar and SettingsSidebar"

This reverts commit 62ea11ae691e83bbe3c5e81c243b8ed89fa96083.

* Channel Icon clean up

* Updated assets/compass-icons files

* Updated channel title component

* ProfilePicture - Code clean up

* UserStatus component - cleaned

* Channel screen fix

* Fix TS issue

* Update index.tsx

* Removed ProfilePicture component

To be added when needed

* Removed UserStatus component

* Added IS_LANDSCAPE constant

* Code review correction

* Fix ts issue

* Added channel.displayName to reinforce security for findAndObserve on potential null teammate profile

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>

* Fix observation on array vs single element

* Refactored ChannelTitle component

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>

* Refactored ChannelGuestLabel

* Refactored ChannelDisplayName

* ChannelTitle cleaned up

* Fix roles check

* Removing unused user utils

* Minor clean up

* Fix TS issue

* Code Refactored.

* Fix render bug in channel_display_name

* Added logout button

* refactored code

Co-authored-by: Avinash Lingaloo <>
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Avinash Lingaloo 2021-07-22 21:41:07 +04:00 committed by GitHub
parent 324dbbd054
commit 64c11580fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 988 additions and 100 deletions

View file

@ -6,7 +6,8 @@ import {getTimeZone} from 'react-native-localize';
import DatabaseManager from '@database/manager';
import {queryUserById} from '@queries/servers/user';
import {updateMe} from '@actions/remote/user';
import User from '@typings/database/models/servers/user';
import type UserModel from '@typings/database/models/servers/user';
export const isTimezoneEnabled = (config: Partial<ClientConfig>) => {
return config?.ExperimentalTimezone === 'true';
@ -33,13 +34,13 @@ export const autoUpdateTimezone = async (serverUrl: string, {deviceTimezone, use
if (currentTimezone.useAutomaticTimezone && newTimezoneExists) {
const timezone = {useAutomaticTimezone: 'true', automaticTimezone: deviceTimezone, manualTimezone: currentTimezone.manualTimezone};
const updatedUser = {...currentUser, timezone} as User;
const updatedUser = {...currentUser, timezone} as UserModel;
await updateMe(serverUrl, updatedUser);
}
return null;
};
export const getUserTimezone = (currentUser: User) => {
export const getUserTimezone = (currentUser: UserModel) => {
if (currentUser?.timezone) {
return {
...currentUser?.timezone,

View file

@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`@components/app_version should match snapshot 1`] = `
<View
pointerEvents="none"
>
<View
style={
Object {
"alignItems": "center",
"justifyContent": "flex-end",
}
}
>
<Text
style={
Object {
"fontSize": 12,
}
}
>
App Version: 0.0.0 (Build 0)
</Text>
</View>
</View>
`;

View file

@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {renderWithIntl} from '@test/intl-test-helper';
import AppVersion from './index';
describe('@components/app_version', () => {
it('should match snapshot', () => {
const wrapper = renderWithIntl(<AppVersion/>);
expect(wrapper.toJSON()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,186 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {StyleProp, Text, View, ViewStyle} from 'react-native';
import CompassIcon from '@components/compass_icon';
import General from '@constants/general';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
type ChannelIconProps = {
hasDraft?: boolean;
isActive?: boolean;
isArchived?: boolean;
isInfo?: boolean;
isUnread?: boolean;
membersCount?: number;
shared: boolean;
size?: number;
style?: StyleProp<ViewStyle>;
testID?: string;
type: string;
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
container: {
alignItems: 'center',
justifyContent: 'center',
},
icon: {
color: changeOpacity(theme.sidebarText, 0.4),
},
iconActive: {
color: theme.sidebarTextActiveColor,
},
iconUnread: {
color: theme.sidebarUnreadText,
},
iconInfo: {
color: theme.centerChannelColor,
},
groupBox: {
alignItems: 'center',
backgroundColor: changeOpacity(theme.sidebarText, 0.16),
borderRadius: 4,
justifyContent: 'center',
},
groupBoxActive: {
backgroundColor: changeOpacity(theme.sidebarTextActiveColor, 0.3),
},
groupBoxUnread: {
backgroundColor: changeOpacity(theme.sidebarUnreadText, 0.3),
},
groupBoxInfo: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.3),
},
group: {
color: theme.sidebarText,
fontSize: 10,
fontWeight: '600',
},
groupActive: {
color: theme.sidebarTextActiveColor,
},
groupUnread: {
color: theme.sidebarUnreadText,
},
groupInfo: {
color: theme.centerChannelColor,
},
};
});
const ChannelIcon = ({
hasDraft = false,
isActive = false,
isArchived = false,
isInfo = false,
isUnread = false,
membersCount = 0,
shared,
size = 12,
style,
testID,
type,
}: ChannelIconProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
let activeIcon;
let unreadIcon;
let activeGroupBox;
let unreadGroupBox;
let activeGroup;
let unreadGroup;
if (isUnread) {
unreadIcon = styles.iconUnread;
unreadGroupBox = styles.groupBoxUnread;
unreadGroup = styles.groupUnread;
}
if (isActive) {
activeIcon = styles.iconActive;
activeGroupBox = styles.groupBoxActive;
activeGroup = styles.groupActive;
}
if (isInfo) {
activeIcon = styles.iconInfo;
activeGroupBox = styles.groupBoxInfo;
activeGroup = styles.groupInfo;
}
let icon;
if (isArchived) {
icon = (
<CompassIcon
name='archive-outline'
style={[styles.icon, unreadIcon, activeIcon, {fontSize: size, left: 1}]}
testID={`${testID}.archive`}
/>
);
} else if (hasDraft) {
icon = (
<CompassIcon
name='pencil-outline'
style={[styles.icon, unreadIcon, activeIcon, {fontSize: size, left: 2}]}
testID={`${testID}.draft`}
/>
);
} else if (shared) {
const iconName = type === General.PRIVATE_CHANNEL ? 'circle-multiple-outline-lock' : 'circle-multiple-outline';
const sharedTestID = type === General.PRIVATE_CHANNEL ? 'channel_icon.shared_private' : 'channel_icon.shared_open';
icon = (
<CompassIcon
name={iconName}
style={[styles.icon, unreadIcon, activeIcon, {fontSize: size, left: 0.5}]}
testID={sharedTestID}
/>
);
} else if (type === General.OPEN_CHANNEL) {
icon = (
<CompassIcon
name='globe'
style={[styles.icon, unreadIcon, activeIcon, {fontSize: size, left: 1}]}
testID={`${testID}.public`}
/>
);
} else if (type === General.PRIVATE_CHANNEL) {
icon = (
<CompassIcon
name='lock-outline'
style={[styles.icon, unreadIcon, activeIcon, {fontSize: size, left: 0.5}]}
testID={`${testID}.private`}
/>
);
} else if (type === General.GM_CHANNEL) {
const fontSize = size - 12;
const boxSize = size - 4;
icon = (
<View
style={[styles.groupBox, unreadGroupBox, activeGroupBox, {width: boxSize, height: boxSize}]}
>
<Text
style={[styles.group, unreadGroup, activeGroup, {fontSize}]}
testID={`${testID}.gm_member_count`}
>
{membersCount}
</Text>
</View>
);
} else if (type === General.DM_CHANNEL) {
//todo: Implement ProfilePicture component
}
return (
<View style={[styles.container, {width: size, height: size}, style]}>
{icon}
</View>
);
};
export default ChannelIcon;

View file

@ -0,0 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {createIconSetFromFontello} from 'react-native-vector-icons';
import fontelloConfig from '@assets/compass-icons.json';
export default createIconSetFromFontello(fontelloConfig, 'compass-icons', 'compass-icons.ttf');

View file

@ -3,6 +3,7 @@
import React from 'react';
import {render} from '@testing-library/react-native';
import {Preferences} from '@constants';
import ErrorText from './index';

View file

@ -2,10 +2,10 @@
// See LICENSE.txt for license information.
import {createElement, isValidElement} from 'react';
import {StyleProp, Text, TextStyle, ViewStyle} from 'react-native';
import {StyleProp, Text, TextProps, TextStyle, ViewStyle} from 'react-native';
import {useIntl} from 'react-intl';
type FormattedTextProps = {
type FormattedTextProps = TextProps & {
id: string;
defaultMessage: string;
values?: Record<string, any>;

View file

@ -0,0 +1,23 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Platform, StatusBar as NativeStatusBar, StatusBarStyle} from 'react-native';
import tinyColor from 'tinycolor2';
type StatusBarProps = {
theme: Theme;
headerColor?: string;
};
const StatusBar = ({theme, headerColor}: StatusBarProps) => {
const headerBarStyle = tinyColor(headerColor ?? theme.sidebarHeaderBg);
let barStyle: StatusBarStyle = 'light-content';
if (headerBarStyle.isLight() && Platform.OS === 'ios') {
barStyle = 'dark-content';
}
return <NativeStatusBar barStyle={barStyle}/>;
};
export default StatusBar;

View file

@ -1,8 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Platform} from 'react-native';
import {FileSystem} from 'react-native-unimodules';
import DeviceInfo from 'react-native-device-info';
import keyMirror from '@utils/key_mirror';
const device = keyMirror({
@ -17,10 +19,11 @@ export default {
...device,
DOCUMENTS_PATH: `${FileSystem.cacheDirectory}/Documents`,
IMAGES_PATH: `${FileSystem.cacheDirectory}/Images`,
IS_IPHONE_WITH_INSETS: Platform.OS === 'ios' && DeviceInfo.hasNotch(),
IS_TABLET: DeviceInfo.isTablet(),
VIDEOS_PATH: `${FileSystem.cacheDirectory}/Videos`,
PERMANENT_SIDEBAR_SETTINGS: '@PERMANENT_SIDEBAR_SETTINGS',
TABLET_WIDTH: 250,
PUSH_NOTIFY_APPLE_REACT_NATIVE: 'apple_rn',
PUSH_NOTIFY_ANDROID_REACT_NATIVE: 'android_rn',
PUSH_NOTIFY_APPLE_REACT_NATIVE: 'apple_rn',
TABLET_WIDTH: 250,
VIDEOS_PATH: `${FileSystem.cacheDirectory}/Videos`,
};

View file

@ -98,6 +98,9 @@ const ViewTypes = keyMirror({
LANDSCAPE: null,
INDICATOR_BAR_VISIBLE: null,
CHANNEL_NAV_BAR_CHANGED: null,
});
const RequiredServer = {

18
app/hooks/did_update.ts Normal file
View file

@ -0,0 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useRef, useEffect, EffectCallback, DependencyList} from 'react';
function useDidUpdate(callback: EffectCallback, deps?: DependencyList) {
const hasMount = useRef(false);
useEffect(() => {
if (hasMount.current) {
callback();
} else {
hasMount.current = true;
}
}, deps);
}
export default useDidUpdate;

View file

@ -0,0 +1,58 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Text} from 'react-native';
import FormattedText from '@components/formatted_text';
import {General} from '@constants';
import {t} from '@utils/i18n';
import {makeStyleSheetFromTheme} from '@utils/theme';
type ChannelDisplayNameProps = {
channelType: string;
currentUserId: string;
displayName: string;
teammateId: string;
theme: Theme;
};
const ChannelDisplayName = ({channelType, currentUserId, displayName, teammateId, theme}: ChannelDisplayNameProps) => {
const style = getStyle(theme);
let isSelfDMChannel = false;
if (channelType === General.DM_CHANNEL && teammateId) {
isSelfDMChannel = currentUserId === teammateId;
}
return (
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={style.text}
testID='channel.nav_bar.title'
>
{isSelfDMChannel ? (
<FormattedText
id={t('channel_header.directchannel.you')}
defaultMessage={'{displayname} (you)'}
values={{displayname: displayName}}
/>) : displayName
}
</Text>
);
};
const getStyle = makeStyleSheetFromTheme((theme) => {
return {
text: {
color: theme.sidebarHeaderTextColor,
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
flex: 0,
flexShrink: 1,
},
};
});
export default ChannelDisplayName;

View file

@ -0,0 +1,71 @@
// 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 FormattedText from '@components/formatted_text';
import {General} from '@constants';
import {t} from '@utils/i18n';
import {makeStyleSheetFromTheme} from '@utils/theme';
type ChannelGuestLabelProps = {
channelType: string;
theme: Theme;
}
const ChannelGuestLabel = ({channelType, theme}: ChannelGuestLabelProps) => {
const style = getStyle(theme);
let messageId;
let defaultMessage;
switch (channelType) {
case General.DM_CHANNEL: {
messageId = t('channel.isGuest');
defaultMessage = 'This person is a guest';
break;
}
case General.GM_CHANNEL: {
messageId = t('channel.hasGuests');
defaultMessage = 'This group message has guests';
break;
}
default : {
messageId = t('channel.channelHasGuests');
defaultMessage = 'This channel has guests';
break;
}
}
return (
<View style={style.guestsWrapper}>
<FormattedText
numberOfLines={1}
ellipsizeMode='tail'
id={messageId}
defaultMessage={defaultMessage}
style={style.guestsText}
/>
</View>
);
};
const getStyle = makeStyleSheetFromTheme((theme) => {
return {
guestsWrapper: {
alignItems: 'flex-start',
flex: 1,
position: 'relative',
top: -1,
width: '90%',
},
guestsText: {
color: theme.sidebarHeaderTextColor,
fontSize: 14,
opacity: 0.6,
},
};
});
export default ChannelGuestLabel;

View file

@ -0,0 +1,190 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {General} from '@constants';
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {isGuest as isTeammateGuest} from '@utils/user';
import React from 'react';
import {TouchableOpacity, View} from 'react-native';
import ChannelIcon from '@components/channel_icon';
import CompassIcon from '@components/compass_icon';
import {MM_TABLES} from '@constants/database';
import {useTheme} from '@context/theme';
import {makeStyleSheetFromTheme} from '@utils/theme';
import ChannelDisplayName from './channel_display_name';
import ChannelGuestLabel from './channel_guest_label';
import type {Database} from '@nozbe/watermelondb';
import type ChannelInfoModel from '@typings/database/models/servers/channel_info';
import type ChannelModel from '@typings/database/models/servers/channel';
import type MyChannelSettingsModel from '@typings/database/models/servers/my_channel_settings';
import type UserModel from '@typings/database/models/servers/user';
type ChannelTitleInputProps = {
canHaveSubtitle: boolean;
channel: ChannelModel;
currentUserId: string;
teammateId?: string;
onPress: () => void;
};
type ChannelTitleProps = ChannelTitleInputProps & {
channelInfo: ChannelInfoModel;
channelSettings: MyChannelSettingsModel;
database: Database;
teammate?: UserModel;
teammateId: string;
};
const ConnectedChannelTitle = ({
canHaveSubtitle,
channel,
channelInfo,
channelSettings,
currentUserId,
onPress,
teammate,
teammateId,
}: ChannelTitleProps) => {
const theme = useTheme();
const style = getStyle(theme);
const channelType = channel.type;
const isArchived = channel.deleteAt !== 0;
const isChannelMuted = channelSettings.notifyProps?.mark_unread === 'mention';
const isChannelShared = false; // todo: Read this value from ChannelModel when implemented
const hasGuests = channelInfo.guestCount > 0;
const teammateRoles = teammate?.roles ?? '';
const isGuest = channelType === General.DM_CHANNEL && isTeammateGuest(teammateRoles);
const showGuestLabel = (canHaveSubtitle || (isGuest && hasGuests) || (channelType === General.DM_CHANNEL && isGuest));
return (
<TouchableOpacity
testID={'channel.title.button'}
style={style.container}
onPress={onPress}
>
<View style={style.wrapper}>
{isArchived && (
<CompassIcon
name='archive-outline'
style={[style.archiveIcon]}
/>
)}
<ChannelDisplayName
channelType={channelType}
currentUserId={currentUserId}
displayName={channel.displayName}
teammateId={teammateId}
theme={theme}
/>
{isChannelShared && (
<ChannelIcon
isActive={true}
isArchived={false}
size={18}
shared={isChannelShared}
style={style.channelIconContainer}
type={channelType}
/>
)}
<CompassIcon
style={style.icon}
size={24}
name='chevron-down'
/>
{isChannelMuted && (
<CompassIcon
style={[style.icon, style.muted]}
size={24}
name='bell-off-outline'
/>
)}
</View>
{showGuestLabel && (
<ChannelGuestLabel
channelType={channelType}
theme={theme}
/>
)}
</TouchableOpacity>
);
};
const getStyle = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
},
wrapper: {
alignItems: 'center',
flex: 1,
position: 'relative',
top: -1,
flexDirection: 'row',
justifyContent: 'flex-start',
width: '90%',
},
icon: {
color: theme.sidebarHeaderTextColor,
marginHorizontal: 1,
},
emoji: {
marginHorizontal: 5,
},
text: {
color: theme.sidebarHeaderTextColor,
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
flex: 0,
flexShrink: 1,
},
channelIconContainer: {
marginLeft: 3,
marginRight: 0,
},
muted: {
marginTop: 1,
opacity: 0.6,
marginLeft: 0,
},
archiveIcon: {
fontSize: 17,
color: theme.sidebarHeaderTextColor,
paddingRight: 7,
},
guestsWrapper: {
alignItems: 'flex-start',
flex: 1,
position: 'relative',
top: -1,
width: '90%',
},
guestsText: {
color: theme.sidebarHeaderTextColor,
fontSize: 14,
opacity: 0.6,
},
};
});
const ChannelTitle: React.FunctionComponent<ChannelTitleInputProps> =
withDatabase(
withObservables(['channel', 'teammateId'], ({channel, teammateId, database}: { channel: ChannelModel; teammateId: string; database: Database }) => {
return {
channelInfo: database.collections.get(MM_TABLES.SERVER.CHANNEL_INFO).findAndObserve(channel.id),
channelSettings: database.collections.get(MM_TABLES.SERVER.MY_CHANNEL_SETTINGS).findAndObserve(channel.id),
...(teammateId && channel.displayName && {teammate: database.collections.get(MM_TABLES.SERVER.USER).findAndObserve(teammateId)}),
};
},
)(ConnectedChannelTitle),
);
export default ChannelTitle;

View file

@ -0,0 +1,108 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {DeviceEventEmitter, LayoutChangeEvent, Platform, useWindowDimensions, View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {useTheme} from '@context/theme';
import VIEWS from '@constants/view';
import DEVICE from '@constants/device';
import {General} from '@constants';
import {getUserIdFromChannelName} from '@utils/user';
import {makeStyleSheetFromTheme} from '@utils/theme';
import ChannelTitle from './channel_title';
import type ChannelModel from '@typings/database/models/servers/channel';
type ChannelNavBar = {
channel: ChannelModel;
currentUserId: string;
onPress: () => void;
config: ClientConfig;
}
const ChannelNavBar = ({currentUserId, channel, onPress}: ChannelNavBar) => {
const insets = useSafeAreaInsets();
const theme = useTheme();
const style = getStyleFromTheme(theme);
const dimensions = useWindowDimensions();
const isLandscape = dimensions.width > dimensions.height;
let height = 0;
let canHaveSubtitle = true;
const onLayout = ({nativeEvent}: LayoutChangeEvent) => {
const {height: layoutHeight} = nativeEvent.layout;
if (height !== layoutHeight && Platform.OS === 'ios') {
height = layoutHeight;
}
DeviceEventEmitter.emit(VIEWS.CHANNEL_NAV_BAR_CHANGED, layoutHeight);
};
switch (Platform.OS) {
case 'android':
height = VIEWS.ANDROID_TOP_PORTRAIT;
if (DEVICE.IS_TABLET) {
height = VIEWS.ANDROID_TOP_LANDSCAPE;
}
break;
case 'ios':
height = VIEWS.IOS_TOP_PORTRAIT - VIEWS.STATUS_BAR_HEIGHT;
if (DEVICE.IS_TABLET && isLandscape) {
height -= 1;
} else if (isLandscape) {
height = VIEWS.IOS_TOP_LANDSCAPE;
canHaveSubtitle = false;
}
if (DEVICE.IS_IPHONE_WITH_INSETS && isLandscape) {
canHaveSubtitle = false;
}
break;
}
let teammateId: string | undefined;
if (channel?.type === General.DM_CHANNEL) {
teammateId = getUserIdFromChannelName(currentUserId, channel.name);
}
return (
<View
onLayout={onLayout}
style={[style.header, {height: height + insets.top, paddingTop: insets.top, paddingLeft: insets.left, paddingRight: insets.right}]}
>
<ChannelTitle
currentUserId={currentUserId}
channel={channel}
onPress={onPress}
canHaveSubtitle={canHaveSubtitle}
teammateId={teammateId}
/>
</View>
);
};
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
header: {
backgroundColor: theme.sidebarHeaderBg,
flexDirection: 'row',
justifyContent: 'flex-start',
width: '100%',
...Platform.select({
android: {
elevation: 10,
},
ios: {
zIndex: 10,
},
}),
},
};
});
export default ChannelNavBar;

View file

@ -1,28 +1,50 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, View} from 'react-native';
import {
Colors,
DebugInstructions,
Header,
LearnMoreLinks,
ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';
import type {LaunchProps} from '@typings/launch';
import {Database} from '@nozbe/watermelondb';
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import React, {useEffect} from 'react';
import {useIntl} from 'react-intl';
import {SafeAreaView} from 'react-native-safe-area-context';
import {logout} from '@actions/remote/general';
import StatusBar from '@components/status_bar';
import ViewTypes from '@constants/view';
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
import {useServerUrl} from '@context/server_url';
import {isMinimumServerVersion} from '@utils/helpers';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {unsupportedServer} from '@utils/supported_server/supported_server';
import {isSystemAdmin as isUserSystemAdmin} from '@utils/user';
import {Colors} from 'react-native/Libraries/NewAppScreen';
import ChannelNavBar from './channel_nav_bar';
import type ChannelModel from '@typings/database/models/servers/channel';
import type SystemModel from '@typings/database/models/servers/system';
import type UserModel from '@typings/database/models/servers/user';
import type {LaunchType} from '@typings/launch';
import {useTheme} from '@context/theme';
import {Text, View} from 'react-native';
type ChannelProps = LaunchProps;
const {SERVER: {CHANNEL, SYSTEM, USER}} = MM_TABLES;
const Channel = (props: ChannelProps) => {
type WithDatabaseArgs = { database: Database }
type WithChannelAndThemeArgs = WithDatabaseArgs & {
currentChannelId: SystemModel;
currentUserId: SystemModel;
}
type ChannelProps = WithDatabaseArgs & {
channel: ChannelModel;
config: SystemModel;
launchType: LaunchType;
user: UserModel;
currentUserId: SystemModel;
};
const Channel = ({channel, user, config, currentUserId}: ChannelProps) => {
// TODO: If we have LaunchProps, ensure we load the correct channel/post/modal.
const {launchType} = props;
console.log(launchType); // eslint-disable-line no-console
// TODO: If LaunchProps.error is true, use the LaunchProps.launchType to determine which
// error message to display. For example:
// if (props.launchError) {
@ -33,69 +55,64 @@ const Channel = (props: ChannelProps) => {
// errorMessage = intl.formatMessage({id: 'mobile.launchError.notification', defaultMessage: 'Did not find a server for this notification'});
// }
// }
const serverUrl = useServerUrl();
//todo: https://mattermost.atlassian.net/browse/MM-37266
const intl = useIntl();
const theme = useTheme();
const styles = getStyleSheet(theme);
useEffect(() => {
const serverVersion = (config.value?.Version) || '';
const isSystemAdmin = isUserSystemAdmin(user.roles);
if (serverVersion) {
const {RequiredServer: {MAJOR_VERSION, MIN_VERSION, PATCH_VERSION}} = ViewTypes;
const isSupportedServer = isMinimumServerVersion(serverVersion, MAJOR_VERSION, MIN_VERSION, PATCH_VERSION);
if (!isSupportedServer) {
// Only display the Alert if the TOS does not need to show first
unsupportedServer(isSystemAdmin, intl.formatMessage);
}
}
}, [config.value?.Version, intl.formatMessage, user.roles]);
const serverUrl = useServerUrl();
const doLogout = () => {
logout(serverUrl!);
};
return (
<>
<StatusBar barStyle='dark-content'/>
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior='automatic'
style={styles.scrollView}
<SafeAreaView
style={styles.flex}
mode='margin'
edges={['left', 'right', 'bottom']}
>
<StatusBar theme={theme}/>
<ChannelNavBar
currentUserId={currentUserId.value}
channel={channel}
onPress={() => null}
config={config.value}
/>
<View style={styles.sectionContainer}>
<Text
onPress={doLogout}
style={styles.sectionTitle}
>
<Header/>
<View style={styles.body}>
<View style={styles.sectionContainer}>
<Text
onPress={doLogout}
style={styles.sectionTitle}
>{`Logout from ${serverUrl}`}</Text>
<Text style={[styles.sectionDescription, {color: theme.centerChannelColor}]}>
{'Edit '}<Text style={[styles.highlight, {color: theme.centerChannelColor}]}>{'screens/channel/index.tsx'}</Text>{' to change this'}
{' screen and then come back to see your edits.'}
</Text>
</View>
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>{'See Your Changes'}</Text>
<Text style={styles.sectionDescription}>
<ReloadInstructions/>
</Text>
</View>
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>{'Debug'}</Text>
<Text style={styles.sectionDescription}>
<DebugInstructions/>
</Text>
</View>
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>{'Learn More'}</Text>
<Text style={styles.sectionDescription}>
{'Read the docs to discover what to do next:'}
</Text>
</View>
<LearnMoreLinks/>
</View>
</ScrollView>
</SafeAreaView>
</>
{`Logout from ${serverUrl}`}
</Text>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
scrollView: {
backgroundColor: Colors.lighter,
},
engine: {
position: 'absolute',
right: 0,
},
body: {
backgroundColor: Colors.white,
const getStyleSheet = makeStyleSheetFromTheme(() => ({
flex: {
flex: 1,
},
sectionContainer: {
marginTop: 32,
@ -106,23 +123,17 @@ const styles = StyleSheet.create({
fontWeight: '600',
color: Colors.black,
},
sectionDescription: {
marginTop: 8,
fontSize: 18,
fontWeight: '400',
color: Colors.dark,
},
highlight: {
fontWeight: '700',
},
footer: {
color: Colors.dark,
fontSize: 12,
fontWeight: '600',
padding: 4,
paddingRight: 12,
textAlign: 'right',
},
});
}));
export default Channel;
export const withSystemIds = withObservables([], ({database}: WithDatabaseArgs) => ({
currentChannelId: database.collections.get(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_CHANNEL_ID),
currentUserId: database.collections.get(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_USER_ID),
config: database.collections.get(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CONFIG),
}));
const withChannelAndUser = withObservables(['currentChannelId'], ({currentChannelId, currentUserId, database}: WithChannelAndThemeArgs) => ({
channel: database.collections.get(CHANNEL).findAndObserve(currentChannelId.value),
user: database.collections.get(USER).findAndObserve(currentUserId.value),
}));
export default withDatabase(withSystemIds(withChannelAndUser(Channel)));

View file

@ -1,17 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withManagedConfig} from '@mattermost/react-native-emm';
import React from 'react';
import {IntlProvider} from 'react-intl';
import {Navigation, NavigationFunctionComponent} from 'react-native-navigation';
import {Platform, StyleProp, ViewStyle} from 'react-native';
import {Navigation, NavigationFunctionComponent} from 'react-native-navigation';
import {gestureHandlerRootHOC} from 'react-native-gesture-handler';
import {withManagedConfig} from '@mattermost/react-native-emm';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import {Screens} from '@constants';
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
import {withServerDatabase} from '@database/components';
// TODO: Remove this and uncomment screens as they get added
@ -38,6 +37,16 @@ const withIntl = (Screen: React.ComponentType) => {
}
}
const withSafeAreaInsets = (Screen: React.ComponentType) => {
return function SafeAreaInsets(props: any){
return (
<SafeAreaProvider>
<Screen {...props} />
</SafeAreaProvider>
)
}
}
Navigation.setLazyComponentRegistrator((screenName) => {
let screen: any|undefined;
let extraStyles: StyleProp<ViewStyle>;
@ -219,6 +228,6 @@ export function registerScreens() {
const channelScreen = require('@screens/channel').default;
const serverScreen = require('@screens/server').default;
Navigation.registerComponent(Screens.CHANNEL, () => withIntl(withServerDatabase(withManagedConfig(channelScreen))));
Navigation.registerComponent(Screens.CHANNEL, () => withSafeAreaInsets(withIntl(withServerDatabase(withManagedConfig(channelScreen)))));
Navigation.registerComponent(Screens.SERVER, () => withIntl(withManagedConfig(serverScreen)));
}

View file

@ -0,0 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Alert} from 'react-native';
import {unsupportedServer} from './supported_server';
describe('Unsupported Server Alert', () => {
const formatMessage = jest.fn();
it('should show the alert for sysadmin', () => {
const alert = jest.spyOn(Alert, 'alert');
unsupportedServer(true, formatMessage);
expect(alert?.mock?.calls?.[0]?.[2]?.length).toBe(2);
});
it('should show the alert for team admin / user', () => {
const alert = jest.spyOn(Alert, 'alert');
unsupportedServer(false, formatMessage);
expect(alert?.mock?.calls?.[0]?.[2]?.length).toBe(1);
});
});

View file

@ -0,0 +1,76 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Alert, AlertButton} from 'react-native';
import ViewTypes from '@constants/view';
import {tryOpenURL} from '@utils/url';
export interface FormatObjectType {
id: string;
defaultMessage: string;
}
export interface FormatMessageType {
(obj: FormatObjectType, values?: any): string;
}
export function unsupportedServer(isSystemAdmin: boolean, formatMessage: FormatMessageType) {
if (isSystemAdmin) {
return unsupportedServerAdminAlert(formatMessage);
}
return unsupportedServerAlert(formatMessage);
}
function unsupportedServerAdminAlert(formatMessage: FormatMessageType) {
const title = formatMessage({id: 'mobile.server_upgrade.title', defaultMessage: 'Server upgrade required'});
const message = formatMessage({
id: 'mobile.server_upgrade.alert_description',
defaultMessage: 'This server version is unsupported and users will be exposed to compatibility issues that cause crashes or severe bugs breaking core functionality of the app. Upgrading to server version {serverVersion} or later is required.',
}, {serverVersion: ViewTypes.RequiredServer.FULL_VERSION});
const cancel: AlertButton = {
text: formatMessage({id: 'mobile.server_upgrade.dismiss', defaultMessage: 'Dismiss'}),
style: 'default',
};
const learnMore: AlertButton = {
text: formatMessage({id: 'mobile.server_upgrade.learn_more', defaultMessage: 'Learn More'}),
style: 'cancel',
onPress: () => {
const url = 'https://docs.mattermost.com/administration/release-lifecycle.html';
const onError = () => {
Alert.alert(
formatMessage({id: 'mobile.link.error.title', defaultMessage: 'Error'}),
formatMessage({id: 'mobile.link.error.text', defaultMessage: 'Unable to open the link.'}),
);
};
tryOpenURL(url, onError);
},
};
const buttons: AlertButton[] = [cancel, learnMore];
const options = {cancelable: false};
Alert.alert(title, message, buttons, options);
}
function unsupportedServerAlert(formatMessage: FormatMessageType) {
const title = formatMessage({id: 'mobile.unsupported_server.title', defaultMessage: 'Unsupported server version'});
const message = formatMessage({
id: 'mobile.unsupported_server.message',
defaultMessage: 'Attachments, link previews, reactions and embed data may not be displayed correctly. If this issue persists contact your System Administrator to upgrade your Mattermost server.',
});
const okButton: AlertButton = {
text: formatMessage({id: 'mobile.unsupported_server.ok', defaultMessage: 'OK'}),
style: 'default',
};
const buttons: AlertButton[] = [okButton];
const options = {cancelable: false};
Alert.alert(title, message, buttons, options);
}

25
app/utils/user/index.ts Normal file
View file

@ -0,0 +1,25 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {General} from '@constants';
export function getUserIdFromChannelName(userId: string, channelName: string): string {
const ids = channelName.split('__');
if (ids[0] === userId) {
return ids[1];
}
return ids[0];
}
export function isRoleInRoles(roles: string, role: string): boolean {
const rolesArray = roles.split(' ');
return rolesArray.includes(role);
}
export function isGuest(roles: string): boolean {
return isRoleInRoles(roles, General.SYSTEM_GUEST_ROLE);
}
export function isSystemAdmin(roles: string): boolean {
return isRoleInRoles(roles, General.SYSTEM_ADMIN_ROLE);
}

View file

@ -454,6 +454,34 @@
"send-outline"
]
},
{
"uid": "4dae60c666dc2fac9f861d9e70166d25",
"css": "circle-multiple-outline-lock",
"code": 59422,
"src": "custom_icons",
"selected": true,
"svg": {
"path": "M916.7 666.7V604.2C916.7 523.6 851.4 458.3 770.8 458.3S625 523.6 625 604.2V666.7C602 666.7 583.3 685.3 583.3 708.3V916.7C583.3 939.7 602 958.3 625 958.3H916.7C939.7 958.3 958.3 939.7 958.3 916.7V708.3C958.3 685.3 939.7 666.7 916.7 666.7ZM687.5 604.2C687.5 558.2 724.9 520.8 770.8 520.8S854.2 558.2 854.2 604.2V666.7H687.5V604.2ZM464.4 214.3C503 184.4 551.5 166.7 604.2 166.7 726.7 166.7 826.4 262.8 832.7 383.7 863.2 392.3 891.2 406.9 915.1 426.3 916.1 416.3 916.7 406.1 916.7 395.8 916.7 223.2 776.7 83.3 604.2 83.3 500.9 83.3 409.3 133.5 352.4 210.7 366.7 209.1 381.2 208.3 395.8 208.3 419.2 208.3 442.1 210.4 464.4 214.3ZM500 807.9C468.7 824 433.4 833.3 395.8 833.3 269.3 833.3 166.7 730.8 166.7 604.2S269.3 375 395.8 375C417.7 375 438.8 378 458.8 383.8 510 398.3 553.4 430.5 583.2 473 599.4 449.9 619.6 429.8 643 414.1 630.4 397.8 616.8 382.3 601.3 368.7 546.4 320.8 474.5 291.7 395.8 291.7 223.2 291.7 83.3 431.6 83.3 604.2S223.2 916.7 395.8 916.7C432.4 916.7 467.4 910.1 500 898.5V807.9ZM413.8 523.5C400.6 503.9 390.4 482.1 383.8 458.8 354.5 461.2 327.7 472.2 305.9 489.4 317.2 525.5 334.9 558.8 357.6 587.9 394.3 635 444.4 670.7 501.9 690.6 506.1 661.3 520 635.2 540.8 616.1 538.3 615.4 535.7 614.6 533.2 613.8 484 597.8 442.1 565.6 413.8 523.5Z",
"width": 1000
},
"search": [
"circle-multiple-outline-lock"
]
},
{
"uid": "a0d8bdcf347c632caf032dc70c78bb2a",
"css": "circle-multiple-outline",
"code": 984725,
"src": "custom_icons",
"selected": true,
"svg": {
"path": "M604.2 708.3C776.7 708.3 916.7 568.4 916.7 395.8 916.7 223.2 776.7 83.3 604.2 83.3 500.9 83.3 409.3 133.5 352.4 210.7 366.7 209.1 381.2 208.3 395.8 208.3 419.2 208.3 442.1 210.4 464.4 214.3 503 184.4 551.5 166.7 604.2 166.7 730.8 166.7 833.3 269.3 833.3 395.8 833.3 522.4 730.8 625 604.2 625 582.3 625 561.2 622 541.2 616.2 538.5 615.5 535.8 614.7 533.2 613.8 484 597.8 442.1 565.6 413.8 523.5 400.6 503.9 390.4 482.1 383.8 458.8 354.5 461.2 327.7 472.2 305.9 489.4 317.2 525.5 334.9 558.8 357.6 587.9 396.2 637.4 449.3 675 510.6 694.1 540.2 703.3 571.6 708.3 604.2 708.3ZM604.2 791.7C618.8 791.7 633.3 790.9 647.6 789.3 590.7 866.5 499.1 916.7 395.8 916.7 223.2 916.7 83.3 776.7 83.3 604.2 83.3 431.6 223.2 291.7 395.8 291.7 474.5 291.7 546.4 320.8 601.3 368.7L601.3 368.7C644.2 406.1 676.7 455 694.1 510.6 672.3 527.8 645.5 538.8 616.2 541.2 594.6 465.3 534.8 405.4 458.8 383.8V383.8C438.8 378 417.7 375 395.8 375 269.3 375 166.7 477.6 166.7 604.2 166.7 730.8 269.3 833.3 395.8 833.3 448.5 833.3 497 815.6 535.7 785.8 557.9 789.6 580.8 791.7 604.2 791.7Z",
"width": 1000
},
"search": [
"circle-multiple-outline"
]
},
{
"uid": "3f2ee509f7e283a723125cb00de0b0ec",
"css": "circle-outline",

Binary file not shown.

View file

@ -28,6 +28,7 @@ module.exports = {
'@context': './app/context',
'@database': './app/database',
'@helpers': './app/helpers',
'@hooks': './app/hooks',
'@i18n': './app/i18n',
'@init': './app/init',
'@notifications': './app/notifications',

View file

@ -12,6 +12,7 @@
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
279A77DD26A6F1BE00B515F1 /* compass-icons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 279A77DC26A6F1BE00B515F1 /* compass-icons.ttf */; };
2D5296A8926B4D7FBAF2D6E2 /* OpenSans-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6561AEAC21CC40B8A72ABB93 /* OpenSans-Light.ttf */; };
49415295267A91230039D64E /* libDatabaseHelper.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 49415294267A911C0039D64E /* libDatabaseHelper.a */; };
49415296267A91290039D64E /* libDatabaseHelper.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 49415294267A911C0039D64E /* libDatabaseHelper.a */; };
@ -163,6 +164,7 @@
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Mattermost/main.m; sourceTree = "<group>"; };
182D203F539AF68F1647EFAF /* Pods-Mattermost-MattermostTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost-MattermostTests.release.xcconfig"; path = "Target Support Files/Pods-Mattermost-MattermostTests/Pods-Mattermost-MattermostTests.release.xcconfig"; sourceTree = "<group>"; };
25BF2BACE89201DE6E585B7E /* Pods-Mattermost.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost.release.xcconfig"; path = "Target Support Files/Pods-Mattermost/Pods-Mattermost.release.xcconfig"; sourceTree = "<group>"; };
279A77DC26A6F1BE00B515F1 /* compass-icons.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "compass-icons.ttf"; path = "../assets/fonts/compass-icons.ttf"; sourceTree = "<group>"; };
297AAFCCF0BD99FC109DA2BC /* Pods-MattermostTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MattermostTests.release.xcconfig"; path = "Target Support Files/Pods-MattermostTests/Pods-MattermostTests.release.xcconfig"; sourceTree = "<group>"; };
32AC3D4EA79E44738A6E9766 /* OpenSans-BoldItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-BoldItalic.ttf"; path = "../assets/fonts/OpenSans-BoldItalic.ttf"; sourceTree = "<group>"; };
34B20A903038487E8D7DEA1E /* Roboto-Light.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Roboto-Light.ttf"; path = "../assets/fonts/Roboto-Light.ttf"; sourceTree = "<group>"; };
@ -456,6 +458,7 @@
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
279A77DC26A6F1BE00B515F1 /* compass-icons.ttf */,
4953BF5F2368AE8600593328 /* SwimeProxy.swift */,
13B07FAE1A68108700A75B9A /* Mattermost */,
7F240A1A220D3A2300637665 /* MattermostShare */,
@ -658,6 +661,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
279A77DD26A6F1BE00B515F1 /* compass-icons.ttf in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
7F0F4B0A24BA173900E14C60 /* LaunchScreen.storyboard in Resources */,
7F292AA71E8ABB1100A450A3 /* splash.png in Resources */,

View file

@ -706,7 +706,7 @@ SPEC CHECKSUMS:
EXFileSystem: 0a04aba8da751b9ac954065911bcf166503f8267
ExpoModulesCore: 2734852616127a6c1fc23012197890a6f3763dc7
FBLazyVector: e686045572151edef46010a6f819ade377dfeb4b
FBReactNativeSpec: cef0cc6d50abc92e8cf52f140aa22b5371cfec0b
FBReactNativeSpec: d35931295aacfe996e833c01a3701d4aa7a80cb4
glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62
jail-monkey: feb2bdedc4d67312cd41a455c22661d804bba985
libwebp: e90b9c01d99205d03b6bb8f2c8c415e5a4ef66f0

View file

@ -43,6 +43,7 @@
"@context/*": ["app/context/*"],
"@database/*": ["app/database/*"],
"@helpers/*": ["app/helpers/*"],
"@hooks/*": ["app/hooks/*"],
"@i18n": ["app/i18n/index"],
"@init/*": ["app/init/*"],
"@notifications": ["app/notifications/index"],