[Gekidou] Update DM and thread title/subtitle as well as navbar rounded corners (#6247)

* Update DM and thread title/subtitle as well as navbar rounded corners

* Fix rounded corners

* ux review
This commit is contained in:
Elias Nahum 2022-05-09 12:45:44 -04:00 committed by GitHub
parent f54f549cbd
commit 591cfa1ae4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 332 additions and 175 deletions

View file

@ -97,13 +97,12 @@ export const switchToThread = async (serverUrl: string, rootId: string) => {
const translations = getTranslations(user.locale);
// Get title translation or default title message
let title = translations[t('thread.header.thread')] || 'Thread';
if (channel.type === General.DM_CHANNEL) {
title = translations[t('thread.header.thread_dm')] || 'Direct Message Thread';
}
const title = translations[t('thread.header.thread')] || 'Thread';
let subtitle = '';
if (channel?.type !== General.DM_CHANNEL) {
if (channel?.type === General.DM_CHANNEL) {
subtitle = channel.displayName;
} else {
// Get translation or default message
subtitle = translations[t('thread.header.thread_in')] || 'in {channelName}';
subtitle = subtitle.replace('{channelName}', channel.displayName);
@ -118,6 +117,10 @@ export const switchToThread = async (serverUrl: string, rootId: string) => {
color: changeOpacity(theme.sidebarHeaderTextColor, 0.72),
text: subtitle,
},
noBorder: true,
scrollEdgeAppearance: {
noBorder: true,
},
rightButtons,
},
});

View file

@ -4,7 +4,7 @@
import React from 'react';
import Animated, {useAnimatedStyle} from 'react-native-reanimated';
import {makeStyleSheetFromTheme} from '@utils/theme';
import RoundedHeaderContext from '@components/rounded_header_context';
type Props = {
defaultHeight: number;
@ -12,50 +12,39 @@ type Props = {
isLargeTitle: boolean;
largeHeight: number;
scrollValue?: Animated.SharedValue<number>;
theme: Theme;
top: number;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
backgroundColor: theme.sidebarBg,
height: 16,
position: 'absolute',
width: '100%',
},
content: {
backgroundColor: theme.centerChannelBg,
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
flex: 1,
},
}));
const NavigationHeaderContext = ({
defaultHeight,
hasSearch,
isLargeTitle,
largeHeight,
scrollValue,
theme,
top,
}: Props) => {
const styles = getStyleSheet(theme);
const marginTop = useAnimatedStyle(() => {
const normal = defaultHeight + top;
const calculated = -(top + (scrollValue?.value || 0));
const searchHeight = hasSearch ? defaultHeight + 9 : 0;
if (!isLargeTitle) {
return {marginTop: Math.max((normal + calculated), normal)};
let margin: number;
if (isLargeTitle) {
margin = Math.max((-(scrollValue?.value || 0) + largeHeight + searchHeight), normal);
} else {
margin = Math.max((normal + calculated), normal);
}
return {marginTop: Math.max((-(scrollValue?.value || 0) + largeHeight + searchHeight), normal)};
return {
position: 'absolute',
width: '100%',
height: '100%',
marginTop: margin,
};
}, [defaultHeight, largeHeight, isLargeTitle, hasSearch]);
return (
<Animated.View style={[styles.container, marginTop]}>
<Animated.View style={styles.content}/>
<Animated.View style={marginTop}>
<RoundedHeaderContext/>
</Animated.View>
);
};

View file

@ -55,11 +55,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
subtitleContainer: {
flexDirection: 'row',
justifyContent: Platform.select({android: 'flex-start', ios: 'center'}),
left: Platform.select({ios: undefined, default: 3}),
},
subtitle: {
color: changeOpacity(theme.sidebarHeaderTextColor, 0.72),
fontFamily: 'OpenSans',
fontSize: 12,
...typography('Body', 75),
lineHeight: 12,
marginBottom: 8,
marginTop: 2,

View file

@ -129,7 +129,6 @@ const NavigationHeader = ({
isLargeTitle={isLargeTitle}
largeHeight={largeHeight}
scrollValue={scrollValue}
theme={theme}
top={insets.top}
/>
}

View file

@ -0,0 +1,36 @@
// 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 {useTheme} from '@context/theme';
import {makeStyleSheetFromTheme} from '@utils/theme';
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
backgroundColor: theme.sidebarBg,
height: '100%',
width: '100%',
position: 'absolute',
},
content: {
backgroundColor: theme.centerChannelBg,
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
flex: 1,
},
}));
const RoundedHeaderContext = () => {
const theme = useTheme();
const styles = getStyleSheet(theme);
return (
<View style={styles.container}>
<View style={styles.content}/>
</View>
);
};
export default RoundedHeaderContext;

View file

@ -1,37 +1,24 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {DeviceEventEmitter, Keyboard, Platform, StyleSheet, View} from 'react-native';
import React, {useEffect, useRef, useState} from 'react';
import {DeviceEventEmitter, StyleSheet, View} from 'react-native';
import {KeyboardTrackingViewRef} from 'react-native-keyboard-tracking-view';
import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
import CompassIcon from '@components/compass_icon';
import FreezeScreen from '@components/freeze_screen';
import NavigationHeader from '@components/navigation_header';
import PostDraft from '@components/post_draft';
import {Events, Navigation} from '@constants';
import {Events} from '@constants';
import {ACCESSORIES_CONTAINER_NATIVE_ID} from '@constants/post_draft';
import {useTheme} from '@context/theme';
import {useAppState, useIsTablet} from '@hooks/device';
import {useDefaultHeaderHeight} from '@hooks/header';
import {popTopScreen} from '@screens/navigation';
import {changeOpacity} from '@utils/theme';
import ChannelPostList from './channel_post_list';
import OtherMentionsBadge from './other_mentions_badge';
import type {HeaderRightButton} from '@components/navigation_header/header';
import ChannelHeader from './header';
type ChannelProps = {
channelId: string;
componentId?: string;
displayName: string;
isOwnDirectMessage: boolean;
memberCount: number;
name: string;
teamId: string;
};
const edges: Edge[] = ['left', 'right'];
@ -42,54 +29,13 @@ const styles = StyleSheet.create({
},
});
const Channel = ({channelId, componentId, displayName, isOwnDirectMessage, memberCount, name, teamId}: ChannelProps) => {
const {formatMessage} = useIntl();
const Channel = ({channelId, componentId}: ChannelProps) => {
const appState = useAppState();
const isTablet = useIsTablet();
const insets = useSafeAreaInsets();
const [shouldRenderPosts, setShouldRenderPosts] = useState(false);
const theme = useTheme();
const defaultHeight = useDefaultHeaderHeight();
const postDraftRef = useRef<KeyboardTrackingViewRef>(null);
const rightButtons: HeaderRightButton[] = useMemo(() => ([{
iconName: 'magnify',
onPress: () => {
DeviceEventEmitter.emit(Navigation.NAVIGATE_TO_TAB, {screen: 'Search', params: {searchTerm: `in: ${name}`}});
if (!isTablet) {
popTopScreen(componentId);
}
},
}, {
iconName: Platform.select({android: 'dots-vertical', default: 'dots-horizontal'}),
onPress: () => true,
buttonType: 'opacity',
}]), [channelId, isTablet, name]);
const leftComponent = useMemo(() => {
if (isTablet || !channelId || !teamId) {
return undefined;
}
return (<OtherMentionsBadge channelId={channelId}/>);
}, [isTablet, channelId, teamId]);
const subtitleCompanion = useMemo(() => (
<CompassIcon
color={changeOpacity(theme.sidebarHeaderTextColor, 0.72)}
name='chevron-right'
size={14}
/>
), []);
const onBackPress = useCallback(() => {
Keyboard.dismiss();
popTopScreen(componentId);
}, []);
const onTitlePress = useCallback(() => {
// eslint-disable-next-line no-console
console.log('Title Press go to Channel Info', displayName);
}, [channelId]);
useEffect(() => {
const listener = DeviceEventEmitter.addListener(Events.PAUSE_KEYBOARD_TRACKING_VIEW, (pause: boolean) => {
@ -104,11 +50,6 @@ const Channel = ({channelId, componentId, displayName, isOwnDirectMessage, membe
return () => listener.remove();
}, []);
let title = displayName;
if (isOwnDirectMessage) {
title = formatMessage({id: 'channel_header.directchannel.you', defaultMessage: '{displayName} (you)'}, {displayName});
}
const marginTop = defaultHeight + (isTablet ? insets.top : 0);
useEffect(() => {
// This is done so that the header renders
@ -128,17 +69,7 @@ const Channel = ({channelId, componentId, displayName, isOwnDirectMessage, membe
edges={edges}
testID='channel.screen'
>
<NavigationHeader
isLargeTitle={false}
leftComponent={leftComponent}
onBackPress={onBackPress}
onTitlePress={onTitlePress}
rightButtons={rightButtons}
showBackButton={!isTablet}
subtitle={formatMessage({id: 'channel', defaultMessage: '{count, plural, one {# member} other {# members}}'}, {count: memberCount})}
subtitleCompanion={subtitleCompanion}
title={title}
/>
<ChannelHeader componentId={componentId}/>
{shouldRenderPosts && Boolean(channelId) &&
<>
<View style={[styles.flex, {marginTop}]}>

View file

@ -0,0 +1,161 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {DeviceEventEmitter, Keyboard, Platform, Text, View} from 'react-native';
import CompassIcon from '@components/compass_icon';
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
import NavigationHeader from '@components/navigation_header';
import {Navigation} from '@constants';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {popTopScreen, showModal} from '@screens/navigation';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import OtherMentionsBadge from './other_mentions_badge';
import type {HeaderRightButton} from '@components/navigation_header/header';
type ChannelProps = {
channelId: string;
customStatus?: UserCustomStatus;
isCustomStatusExpired: boolean;
componentId?: string;
displayName: string;
isOwnDirectMessage: boolean;
memberCount?: number;
searchTerm: string;
teamId: string;
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
customStatusContainer: {
flexDirection: 'row',
height: 13,
left: Platform.select({ios: undefined, default: -2}),
marginTop: Platform.select({ios: undefined, default: 1}),
},
customStatusEmoji: {marginRight: 5},
customStatusText: {
alignItems: 'center',
height: 13,
},
subtitle: {
color: changeOpacity(theme.sidebarHeaderTextColor, 0.72),
...typography('Body', 75),
lineHeight: 12,
marginBottom: 8,
marginTop: 2,
height: 13,
},
}));
const ChannelHeader = ({
channelId, componentId, customStatus, displayName,
isCustomStatusExpired, isOwnDirectMessage, memberCount,
searchTerm, teamId,
}: ChannelProps) => {
const {formatMessage} = useIntl();
const isTablet = useIsTablet();
const theme = useTheme();
const styles = getStyleSheet(theme);
const leftComponent = useMemo(() => {
if (isTablet || !channelId || !teamId) {
return undefined;
}
return (<OtherMentionsBadge channelId={channelId}/>);
}, [isTablet, channelId, teamId]);
const rightButtons: HeaderRightButton[] = useMemo(() => ([{
iconName: 'magnify',
onPress: () => {
DeviceEventEmitter.emit(Navigation.NAVIGATE_TO_TAB, {screen: 'Search', params: {searchTerm: `in: ${searchTerm}`}});
if (!isTablet) {
popTopScreen(componentId);
}
},
}, {
iconName: Platform.select({android: 'dots-vertical', default: 'dots-horizontal'}),
onPress: () => true,
buttonType: 'opacity',
}]), [isTablet, searchTerm]);
const onBackPress = useCallback(() => {
Keyboard.dismiss();
popTopScreen(componentId);
}, []);
const onTitlePress = useCallback(() => {
// eslint-disable-next-line no-console
console.log('Title Press go to Channel Info');
showModal('ChannelInfo', '', {channelId});
}, [channelId]);
let title = displayName;
if (isOwnDirectMessage) {
title = formatMessage({id: 'channel_header.directchannel.you', defaultMessage: '{displayName} (you)'}, {displayName});
}
let subtitle;
if (memberCount) {
subtitle = formatMessage({id: 'channel', defaultMessage: '{count, plural, one {# member} other {# members}}'}, {count: memberCount});
} else if (!customStatus || !customStatus.text || isCustomStatusExpired) {
subtitle = formatMessage({id: 'channel.details', defaultMessage: 'View details'});
}
const subtitleCompanion = useMemo(() => {
if (memberCount || !customStatus || !customStatus.text || isCustomStatusExpired) {
return (
<CompassIcon
color={changeOpacity(theme.sidebarHeaderTextColor, 0.72)}
name='chevron-right'
size={14}
/>
);
} else if (customStatus && customStatus.text) {
return (
<View style={styles.customStatusContainer}>
{Boolean(customStatus.emoji) &&
<CustomStatusEmoji
customStatus={customStatus}
emojiSize={13}
style={styles.customStatusEmoji}
/>
}
<View style={styles.customStatusText}>
<Text
numberOfLines={1}
ellipsizeMode='tail'
style={styles.subtitle}
>
{customStatus.text}
</Text>
</View>
</View>
);
}
return undefined;
}, [memberCount, customStatus, isCustomStatusExpired]);
return (
<NavigationHeader
isLargeTitle={false}
leftComponent={leftComponent}
onBackPress={onBackPress}
onTitlePress={onTitlePress}
rightButtons={rightButtons}
showBackButton={!isTablet}
subtitle={subtitle}
subtitleCompanion={subtitleCompanion}
title={title}
/>
);
};
export default ChannelHeader;

View file

@ -0,0 +1,87 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {of as of$} from 'rxjs';
import {combineLatestWith, switchMap} from 'rxjs/operators';
import {General} from '@constants';
import {observeChannel, observeChannelInfo} from '@queries/servers/channel';
import {observeCurrentChannelId, observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
import {observeUser} from '@queries/servers/user';
import {getUserCustomStatus, getUserIdFromChannelName, isCustomStatusExpired as checkCustomStatusIsExpired} from '@utils/user';
import ChannelHeader from './header';
import type {WithDatabaseArgs} from '@typings/database/database';
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
const currentUserId = observeCurrentUserId(database);
const channelId = observeCurrentChannelId(database);
const teamId = observeCurrentTeamId(database);
const channel = channelId.pipe(
switchMap((id) => observeChannel(database, id)),
);
const channelInfo = channelId.pipe(
switchMap((id) => observeChannelInfo(database, id)),
);
const dmUser = currentUserId.pipe(
combineLatestWith(channel),
switchMap(([userId, c]) => {
if (c?.type === General.DM_CHANNEL) {
const teammateId = getUserIdFromChannelName(userId, c.name);
return observeUser(database, teammateId);
}
return of$(undefined);
}),
);
const isOwnDirectMessage = currentUserId.pipe(
combineLatestWith(dmUser),
switchMap(([userId, dm]) => of$(userId === dm?.id)),
);
const customStatus = dmUser.pipe(
switchMap((dm) => of$(getUserCustomStatus(dm))),
);
const isCustomStatusExpired = dmUser.pipe(
switchMap((dm) => of$(checkCustomStatusIsExpired(dm))),
);
const searchTerm = channel.pipe(
combineLatestWith(dmUser),
switchMap(([c, dm]) => {
if (c?.type === General.DM_CHANNEL) {
return of$(dm ? `@${dm.username}` : '');
} else if (c?.type === General.GM_CHANNEL) {
return of$(`@${c.name}`);
}
return of$(c?.name);
}),
);
const displayName = channel.pipe(switchMap((c) => of$(c?.displayName)));
const memberCount = channelInfo.pipe(
combineLatestWith(dmUser),
switchMap(([ci, dm]) => of$(dm ? undefined : ci?.memberCount)));
return {
channelId,
customStatus,
displayName,
isCustomStatusExpired,
isOwnDirectMessage,
memberCount,
searchTerm,
teamId,
};
});
export default withDatabase(enhanced(ChannelHeader));

View file

@ -17,6 +17,9 @@ type Props = {
}
const styles = StyleSheet.create({
container: {
minWidth: 24,
},
badge: {
left: 2,
position: 'relative',
@ -91,7 +94,7 @@ const OtherMentionsBadge = ({channelId}: Props) => {
}, []);
return (
<View>
<View style={styles.container}>
<Badge
type='Small'
visible={count > 0}

View file

@ -3,67 +3,15 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {combineLatest, of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {General} from '@constants';
import {observeChannel, observeChannelInfo} from '@queries/servers/channel';
import {observeCurrentChannelId, observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
import {observeUser} from '@queries/servers/user';
import {getUserIdFromChannelName} from '@utils/user';
import {observeCurrentChannelId} from '@queries/servers/system';
import Channel from './channel';
import type {WithDatabaseArgs} from '@typings/database/database';
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
const currentUserId = observeCurrentUserId(database);
const channelId = observeCurrentChannelId(database);
const teamId = observeCurrentTeamId(database);
const channel = channelId.pipe(
switchMap((id) => observeChannel(database, id)),
);
const channelInfo = channelId.pipe(
switchMap((id) => observeChannelInfo(database, id)),
);
const isOwnDirectMessage = combineLatest([currentUserId, channel]).pipe(
switchMap(([userId, ch]) => {
if (ch?.type === General.DM_CHANNEL) {
const teammateId = getUserIdFromChannelName(userId, ch.name);
return of$(userId === teammateId);
}
return of$(false);
}),
);
const displayName = channel.pipe(switchMap((c) => of$(c?.displayName)));
const name = combineLatest([currentUserId, channel]).pipe(switchMap(([userId, c]) => {
if (c?.type === General.DM_CHANNEL) {
const teammateId = getUserIdFromChannelName(userId, c.name);
return observeUser(database, teammateId).pipe(
// eslint-disable-next-line max-nested-callbacks
switchMap((u) => (u ? of$(`@${u.username}`) : of$('Someone'))),
);
} else if (c?.type === General.GM_CHANNEL) {
return of$(`@${c.name}`);
}
return of$(c?.name);
}));
const memberCount = channelInfo.pipe(switchMap((ci) => of$(ci?.memberCount || 0)));
return {
channelId,
displayName,
isOwnDirectMessage,
memberCount,
name,
teamId,
};
});
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
channelId: observeCurrentChannelId(database),
}));
export default withDatabase(enhanced(Channel));

View file

@ -131,10 +131,13 @@ Navigation.setDefaultOptions({
fontSize: 18,
fontWeight: '600',
},
backButton: {
enableMenu: false,
},
subtitle: {
fontFamily: 'OpenSans',
fontSize: 12,
fontWeight: '500',
fontWeight: '400',
},
},
});

View file

@ -8,6 +8,7 @@ import {Edge, SafeAreaView} from 'react-native-safe-area-context';
import FreezeScreen from '@components/freeze_screen';
import PostDraft from '@components/post_draft';
import RoundedHeaderContext from '@components/rounded_header_context';
import {THREAD_ACCESSORIES_CONTAINER_NATIVE_ID} from '@constants/post_draft';
import {useAppState} from '@hooks/device';
import useDidUpdate from '@hooks/did_update';
@ -24,15 +25,12 @@ type ThreadProps = {
const edges: Edge[] = ['left', 'right'];
const getStyleSheet = StyleSheet.create(() => ({
flex: {
flex: 1,
},
}));
const styles = StyleSheet.create({
flex: {flex: 1},
});
const Thread = ({componentId, rootPost}: ThreadProps) => {
const appState = useAppState();
const styles = getStyleSheet();
const postDraftRef = useRef<KeyboardTrackingViewRef>(null);
useDidUpdate(() => {
@ -49,6 +47,7 @@ const Thread = ({componentId, rootPost}: ThreadProps) => {
edges={edges}
testID='thread.screen'
>
<RoundedHeaderContext/>
{Boolean(rootPost?.id) &&
<>
<View style={styles.flex}>

View file

@ -107,6 +107,7 @@
"channel_modal.optional": "(optional)",
"channel_modal.purpose": "Purpose",
"channel_modal.purposeEx": "A channel to file bugs and improvements",
"channel.details": "View details",
"combined_system_message.added_to_channel.many_expanded": "{users} and {lastUser} were **added to the channel** by {actor}.",
"combined_system_message.added_to_channel.one": "{firstUser} **added to the channel** by {actor}.",
"combined_system_message.added_to_channel.one_you": "You were **added to the channel** by {actor}.",
@ -477,8 +478,6 @@
"mobile.unsupported_server.title": "Unsupported server version",
"mobile.user_list.deactivated": "Deactivated",
"mobile.write_storage_permission_denied_description": "Save files to your device. Open Settings to grant {applicationName} write access to files on this device.",
"mobile.youtube_playback_error.description": "An error occurred while trying to play the YouTube video.\nDetails: {details}",
"mobile.youtube_playback_error.title": "YouTube playback error",
"modal.manual_status.auto_responder.message_": "Would you like to switch your status to \"{status}\" and disable Automatic Replies?",
"modal.manual_status.auto_responder.message_away": "Would you like to switch your status to \"Away\" and disable Automatic Replies?",
"modal.manual_status.auto_responder.message_dnd": "Would you like to switch your status to \"Do Not Disturb\" and disable Automatic Replies?",
@ -576,7 +575,6 @@
"team_list.no_other_teams.description": "To join another team, ask a Team Admin for an invitation, or create your own team.",
"team_list.no_other_teams.title": "No additional teams to join",
"thread.header.thread": "Thread",
"thread.header.thread_dm": "Direct Message Thread",
"thread.header.thread_in": "in {channelName}",
"thread.noReplies": "No replies yet",
"thread.options.title": "THREAD ACTIONS",