From 591cfa1ae4914cb4b9eddefdfb30aa578d7ad90c Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Mon, 9 May 2022 12:45:44 -0400 Subject: [PATCH] [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 --- app/actions/local/thread.ts | 13 +- app/components/navigation_header/context.tsx | 39 ++--- app/components/navigation_header/header.tsx | 4 +- app/components/navigation_header/index.tsx | 1 - .../rounded_header_context/index.tsx | 36 ++++ app/screens/channel/channel.tsx | 81 +-------- app/screens/channel/header/header.tsx | 161 ++++++++++++++++++ app/screens/channel/header/index.ts | 87 ++++++++++ .../other_mentions_badge/index.tsx | 5 +- app/screens/channel/index.tsx | 60 +------ app/screens/navigation.ts | 5 +- app/screens/thread/thread.tsx | 11 +- assets/base/i18n/en.json | 4 +- 13 files changed, 332 insertions(+), 175 deletions(-) create mode 100644 app/components/rounded_header_context/index.tsx create mode 100644 app/screens/channel/header/header.tsx create mode 100644 app/screens/channel/header/index.ts rename app/screens/channel/{ => header}/other_mentions_badge/index.tsx (97%) diff --git a/app/actions/local/thread.ts b/app/actions/local/thread.ts index e9b9b15e1..8456237b4 100644 --- a/app/actions/local/thread.ts +++ b/app/actions/local/thread.ts @@ -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, }, }); diff --git a/app/components/navigation_header/context.tsx b/app/components/navigation_header/context.tsx index 6bafb1528..d8e84ba6d 100644 --- a/app/components/navigation_header/context.tsx +++ b/app/components/navigation_header/context.tsx @@ -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; - 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 ( - - + + ); }; diff --git a/app/components/navigation_header/header.tsx b/app/components/navigation_header/header.tsx index 1fc807277..b1af2f609 100644 --- a/app/components/navigation_header/header.tsx +++ b/app/components/navigation_header/header.tsx @@ -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, diff --git a/app/components/navigation_header/index.tsx b/app/components/navigation_header/index.tsx index 749fff202..301f3087e 100644 --- a/app/components/navigation_header/index.tsx +++ b/app/components/navigation_header/index.tsx @@ -129,7 +129,6 @@ const NavigationHeader = ({ isLargeTitle={isLargeTitle} largeHeight={largeHeight} scrollValue={scrollValue} - theme={theme} top={insets.top} /> } diff --git a/app/components/rounded_header_context/index.tsx b/app/components/rounded_header_context/index.tsx new file mode 100644 index 000000000..65b36c0ea --- /dev/null +++ b/app/components/rounded_header_context/index.tsx @@ -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 ( + + + + ); +}; + +export default RoundedHeaderContext; diff --git a/app/screens/channel/channel.tsx b/app/screens/channel/channel.tsx index 2e0d9f986..164e57c1f 100644 --- a/app/screens/channel/channel.tsx +++ b/app/screens/channel/channel.tsx @@ -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(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 (); - }, [isTablet, channelId, teamId]); - - const subtitleCompanion = useMemo(() => ( - - ), []); - - 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' > - + {shouldRenderPosts && Boolean(channelId) && <> diff --git a/app/screens/channel/header/header.tsx b/app/screens/channel/header/header.tsx new file mode 100644 index 000000000..2697332d0 --- /dev/null +++ b/app/screens/channel/header/header.tsx @@ -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 (); + }, [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 ( + + ); + } else if (customStatus && customStatus.text) { + return ( + + {Boolean(customStatus.emoji) && + + } + + + {customStatus.text} + + + + ); + } + + return undefined; + }, [memberCount, customStatus, isCustomStatusExpired]); + + return ( + + ); +}; + +export default ChannelHeader; diff --git a/app/screens/channel/header/index.ts b/app/screens/channel/header/index.ts new file mode 100644 index 000000000..64faafa5e --- /dev/null +++ b/app/screens/channel/header/index.ts @@ -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)); diff --git a/app/screens/channel/other_mentions_badge/index.tsx b/app/screens/channel/header/other_mentions_badge/index.tsx similarity index 97% rename from app/screens/channel/other_mentions_badge/index.tsx rename to app/screens/channel/header/other_mentions_badge/index.tsx index a85408d6e..3beda4db7 100644 --- a/app/screens/channel/other_mentions_badge/index.tsx +++ b/app/screens/channel/header/other_mentions_badge/index.tsx @@ -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 ( - + 0} diff --git a/app/screens/channel/index.tsx b/app/screens/channel/index.tsx index 984f4de1c..0c797e500 100644 --- a/app/screens/channel/index.tsx +++ b/app/screens/channel/index.tsx @@ -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)); diff --git a/app/screens/navigation.ts b/app/screens/navigation.ts index 2f89cd2d6..984725743 100644 --- a/app/screens/navigation.ts +++ b/app/screens/navigation.ts @@ -131,10 +131,13 @@ Navigation.setDefaultOptions({ fontSize: 18, fontWeight: '600', }, + backButton: { + enableMenu: false, + }, subtitle: { fontFamily: 'OpenSans', fontSize: 12, - fontWeight: '500', + fontWeight: '400', }, }, }); diff --git a/app/screens/thread/thread.tsx b/app/screens/thread/thread.tsx index 59a567e64..cb756fee8 100644 --- a/app/screens/thread/thread.tsx +++ b/app/screens/thread/thread.tsx @@ -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(null); useDidUpdate(() => { @@ -49,6 +47,7 @@ const Thread = ({componentId, rootPost}: ThreadProps) => { edges={edges} testID='thread.screen' > + {Boolean(rootPost?.id) && <> diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 5500ba799..6e60fcea0 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -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",