From 65366e53a93abd2718109f460e7ec5c1a4dd7904 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Tue, 17 May 2022 11:07:46 -0400 Subject: [PATCH] Gekidou fixes (#6274) * Add error boundry around latex to avoid crash * Do not update channel display name when displayName is empty * Show Muted unread channel as muted unless it has mentions * Video size to wrapperWidth if thumbnail is not available * Hide Threads button in channel list when only unreads filter if it does not have any unreads or mentions * Ensure channel_item muted state is bolded when unread * Bump network default retry to 3 attempts * Sort only unreads by last root post if CRT is enabled * Default canPost to true if permission is not found * rename to ErrorBoundary (typo fix) * simplify filterAndSortMyChannels * Fix channel name collision with mention badges --- app/actions/local/channel.ts | 2 +- .../__snapshots__/channel_item.test.tsx.snap | 16 +++-- app/components/channel_item/channel_item.tsx | 30 ++++++---- app/components/markdown/error_boundary.tsx | 58 +++++++++++++++++++ .../markdown/markdown_latex_block/index.tsx | 36 +++++++----- .../markdown/markdown_latex_inline/index.tsx | 36 ++++++++---- app/components/post_draft/index.ts | 2 +- .../post_list/post/body/files/files.tsx | 2 +- .../post_list/post/body/files/video_file.tsx | 8 ++- app/managers/network_manager.ts | 2 +- app/queries/servers/post.ts | 29 +++++++++- .../categories/unreads/index.ts | 35 ++++++++--- .../threads_button.test.tsx.snap | 7 ++- .../categories_list/threads_button/index.ts | 3 +- .../threads_button/threads_button.test.tsx | 40 +++++++++---- .../threads_button/threads_button.tsx | 9 ++- assets/base/i18n/en.json | 2 + 17 files changed, 241 insertions(+), 76 deletions(-) create mode 100644 app/components/markdown/error_boundary.tsx diff --git a/app/actions/local/channel.ts b/app/actions/local/channel.ts index 6f60b32ee..ed7aee28b 100644 --- a/app/actions/local/channel.ts +++ b/app/actions/local/channel.ts @@ -410,7 +410,7 @@ export async function updateChannelsDisplayName(serverUrl: string, channels: Cha } } - if (channel.displayName !== newDisplayName) { + if (newDisplayName && channel.displayName !== newDisplayName) { channel.prepareUpdate((c) => { c.displayName = extractChannelDisplayName({ type: c.type, diff --git a/app/components/channel_item/__snapshots__/channel_item.test.tsx.snap b/app/components/channel_item/__snapshots__/channel_item.test.tsx.snap index 11913eb81..8486e0a5c 100644 --- a/app/components/channel_item/__snapshots__/channel_item.test.tsx.snap +++ b/app/components/channel_item/__snapshots__/channel_item.test.tsx.snap @@ -93,12 +93,14 @@ exports[`components/channel_list/categories/body/channel_item should match snaps Object { "color": "rgba(255,255,255,0.72)", "marginTop": -1, - "paddingHorizontal": 12, + "paddingLeft": 12, + "paddingRight": 20, }, false, + null, + null, + false, false, - null, - null, ] } testID="channel_list_item.hello.display_name" @@ -204,12 +206,14 @@ exports[`components/channel_list/categories/body/channel_item should match snaps Object { "color": "rgba(255,255,255,0.72)", "marginTop": -1, - "paddingHorizontal": 12, + "paddingLeft": 12, + "paddingRight": 20, }, false, + null, + null, + false, false, - null, - null, ] } testID="channel_list_item.hello.display_name" diff --git a/app/components/channel_item/channel_item.tsx b/app/components/channel_item/channel_item.tsx index 3037efe61..a86f8fc13 100644 --- a/app/components/channel_item/channel_item.tsx +++ b/app/components/channel_item/channel_item.tsx @@ -56,7 +56,8 @@ export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ text: { marginTop: -1, color: changeOpacity(theme.sidebarText, 0.72), - paddingHorizontal: 12, + paddingLeft: 12, + paddingRight: 20, }, highlight: { color: theme.sidebarText, @@ -68,6 +69,9 @@ export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ muted: { color: changeOpacity(theme.sidebarText, 0.32), }, + mutedInfo: { + color: changeOpacity(theme.centerChannelColor, 0.32), + }, badge: { borderColor: theme.sidebarBg, position: 'relative', @@ -99,6 +103,9 @@ export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ marginTop: 4, ...typography('Body', 75), }, + teamNameMuted: { + color: changeOpacity(theme.centerChannelColor, 0.32), + }, teamNameTablet: { marginLeft: -12, paddingLeft: 0, @@ -109,7 +116,7 @@ export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ })); export const textStyle = StyleSheet.create({ - bright: typography('Body', 200, 'SemiBold'), + bold: typography('Body', 200, 'SemiBold'), regular: typography('Body', 200, 'Regular'), }); @@ -122,8 +129,8 @@ const ChannelListItem = ({ const isTablet = useIsTablet(); const styles = getStyleSheet(theme); - // Make it brighter if it's not muted, and highlighted or has unreads - const isBright = isUnread || mentionsCount > 0; + // Make it bolded if it has unreads or mentions + const isBolded = isUnread || mentionsCount > 0; const height = useMemo(() => { let h = 40; @@ -138,13 +145,14 @@ const ChannelListItem = ({ }, [channel.id]); const textStyles = useMemo(() => [ - isBright ? textStyle.bright : textStyle.regular, + isBolded ? textStyle.bold : textStyle.regular, styles.text, - isBright && styles.highlight, - isMuted && styles.muted, + isBolded && styles.highlight, isActive && isTablet && !isInfo ? styles.textActive : null, isInfo ? styles.textInfo : null, - ], [isBright, styles, isMuted, isActive, isInfo, isTablet]); + isMuted && styles.muted, + isMuted && isInfo && styles.infoMuted, + ], [isBolded, styles, isMuted, isActive, isInfo, isTablet]); const containerStyle = useMemo(() => [ styles.container, @@ -178,7 +186,7 @@ const ChannelListItem = ({ hasDraft={hasDraft} isActive={isInfo ? false : isTablet && isActive} isInfo={isInfo} - isUnread={isBright} + isUnread={isBolded} isArchived={channel.deleteAt > 0} membersCount={membersCount} name={channel.name} @@ -201,7 +209,7 @@ const ChannelListItem = ({ ellipsizeMode='tail' numberOfLines={1} testID={`${testID}.${teamDisplayName}.display_name`} - style={styles.teamName} + style={[styles.teamName, isMuted && styles.teamNameMuted]} > {teamDisplayName} @@ -218,7 +226,7 @@ const ChannelListItem = ({ ellipsizeMode='tail' numberOfLines={1} testID={`${testID}.${teamDisplayName}.display_name`} - style={[styles.teamName, styles.teamNameTablet]} + style={[styles.teamName, styles.teamNameTablet, isMuted && styles.teamNameMuted]} > {teamDisplayName} diff --git a/app/components/markdown/error_boundary.tsx b/app/components/markdown/error_boundary.tsx new file mode 100644 index 000000000..0dffd3d79 --- /dev/null +++ b/app/components/markdown/error_boundary.tsx @@ -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 {makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; + +type State = { + hasError: boolean; +} + +type Props = { + children: JSX.Element; + error: string; + theme: Theme; +} + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ + error: { + color: theme.errorTextColor, + flexDirection: 'row', + flexWrap: 'wrap', + fontStyle: 'italic', + ...typography('Body', 100), + }, +})); + +class ErrorBoundary extends React.PureComponent { + constructor(props: Props) { + super(props); + this.state = {hasError: false}; + } + + componentDidCatch() { + this.setState({hasError: true}); + } + + render() { + // eslint-disable-next-line react/prop-types + const {children, error, theme} = this.props; + const {hasError} = this.state; + const style = getStyleSheet(theme); + + if (hasError) { + return ( + + {error} + + ); + } + + return children; + } +} + +export default ErrorBoundary; diff --git a/app/components/markdown/markdown_latex_block/index.tsx b/app/components/markdown/markdown_latex_block/index.tsx index 55420036c..690c7a548 100644 --- a/app/components/markdown/markdown_latex_block/index.tsx +++ b/app/components/markdown/markdown_latex_block/index.tsx @@ -10,6 +10,7 @@ import MathView from 'react-native-math-view'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import FormattedText from '@components/formatted_text'; +import ErrorBoundary from '@components/markdown/error_boundary'; import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item'; import TouchableWithFeedback from '@components/touchable_with_feedback'; import {Screens} from '@constants'; @@ -194,21 +195,26 @@ const LatexCodeBlock = ({content, theme}: Props) => { type={'opacity'} > - - {split.lines?.map((latexCode) => ( - - - - ))} - {plusMoreLines} - + + + {split.lines?.map((latexCode) => ( + + + + ))} + {plusMoreLines} + + {languageDisplayName} diff --git a/app/components/markdown/markdown_latex_inline/index.tsx b/app/components/markdown/markdown_latex_inline/index.tsx index b7f532901..2fe2d272f 100644 --- a/app/components/markdown/markdown_latex_inline/index.tsx +++ b/app/components/markdown/markdown_latex_inline/index.tsx @@ -2,10 +2,13 @@ // See LICENSE.txt for license information. import React from 'react'; +import {useIntl} from 'react-intl'; import {Platform, Text, View} from 'react-native'; import MathView from 'react-native-math-view'; +import ErrorBoundary from '@components/markdown/error_boundary'; import {makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; type Props = { content: string; @@ -27,32 +30,41 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { flexWrap: 'wrap', }, errorText: { - flexDirection: 'row', color: theme.errorTextColor, + flexDirection: 'row', flexWrap: 'wrap', + fontStyle: 'italic', + ...typography('Body', 100), }, }; }); const LatexInline = ({content, maxMathWidth, theme}: Props) => { + const {formatMessage} = useIntl(); const style = getStyleSheet(theme); const onRenderErrorMessage = (errorMsg: MathViewErrorProps) => { - return {'Latex render error: ' + errorMsg.error.message}; + const error = formatMessage({id: 'markdown.latex.error', defaultMessage: 'Latex render error'}); + return {`${error}: ${errorMsg.error.message}`}; }; return ( - - - + + + + ); }; diff --git a/app/components/post_draft/index.ts b/app/components/post_draft/index.ts index e1721db36..e296aae23 100644 --- a/app/components/post_draft/index.ts +++ b/app/components/post_draft/index.ts @@ -50,7 +50,7 @@ const enhanced = withObservables([], (ownProps: WithDatabaseArgs & OwnProps) => switchMap((id) => observeChannel(database, id!)), ); - const canPost = combineLatest([channel, currentUser]).pipe(switchMap(([c, u]) => (c && u ? observePermissionForChannel(c, u, Permissions.CREATE_POST, false) : of$(false)))); + const canPost = combineLatest([channel, currentUser]).pipe(switchMap(([c, u]) => (c && u ? observePermissionForChannel(c, u, Permissions.CREATE_POST, true) : of$(true)))); const channelIsArchived = channel.pipe(switchMap((c) => (ownProps.channelIsArchived ? of$(true) : of$(c?.deleteAt !== 0)))); const experimentalTownSquareIsReadOnly = observeConfigBooleanValue(database, 'ExperimentalTownSquareIsReadOnly'); diff --git a/app/components/post_list/post/body/files/files.tsx b/app/components/post_list/post/body/files/files.tsx index 0d5c5a893..e1d3a4416 100644 --- a/app/components/post_list/post/body/files/files.tsx +++ b/app/components/post_list/post/body/files/files.tsx @@ -95,7 +95,7 @@ const Files = ({canDownloadFiles, failed, filesInfo, isReplyPost, layoutWidth, l filesForGallery.value[idx] = file; }; - const isSingleImage = () => (filesInfo.length === 1 && isImage(filesInfo[0])); + const isSingleImage = () => (filesInfo.length === 1 && (isImage(filesInfo[0]) || isVideo(filesInfo[0]))); const renderItems = (items: FileInfo[], moreImagesCount = 0, includeGutter = false) => { const singleImage = isSingleImage(); diff --git a/app/components/post_list/post/body/files/video_file.tsx b/app/components/post_list/post/body/files/video_file.tsx index 3f5f8d9a2..166116e3a 100644 --- a/app/components/post_list/post/body/files/video_file.tsx +++ b/app/components/post_list/post/body/files/video_file.tsx @@ -26,7 +26,7 @@ type Props = { inViewPort?: boolean; isSingleImage?: boolean; resizeMode?: ResizeMode; - wrapperWidth?: number; + wrapperWidth: number; updateFileForGallery: (idx: number, file: FileInfo) => void; } @@ -74,7 +74,7 @@ const VideoFile = ({ const imageDimensions = useMemo(() => { if (isSingleImage) { const viewPortHeight = Math.max(dimensions.height, dimensions.width) * 0.45; - return calculateDimensions(video.height, video.width, wrapperWidth, viewPortHeight); + return calculateDimensions(video.height || wrapperWidth, video.width || wrapperWidth, wrapperWidth, viewPortHeight); } return undefined; @@ -106,6 +106,10 @@ const VideoFile = ({ setVideo(data); } } finally { + if (!data.width) { + data.height = wrapperWidth; + data.width = wrapperWidth; + } const {width: tw, height: th} = calculateDimensions( data.height, data.width, diff --git a/app/managers/network_manager.ts b/app/managers/network_manager.ts index a7617ec39..f7d69631c 100644 --- a/app/managers/network_manager.ts +++ b/app/managers/network_manager.ts @@ -41,7 +41,7 @@ class NetworkManager { }, retryPolicyConfiguration: { type: RetryTypes.EXPONENTIAL_RETRY, - retryLimit: 2, + retryLimit: 3, exponentialBackoffBase: 2, exponentialBackoffScale: 0.5, }, diff --git a/app/queries/servers/post.ts b/app/queries/servers/post.ts index b2ba8c046..5731f2fea 100644 --- a/app/queries/servers/post.ts +++ b/app/queries/servers/post.ts @@ -14,7 +14,7 @@ import {queryPreferencesByCategoryAndName} from './preference'; import type PostInChannelModel from '@typings/database/models/servers/posts_in_channel'; import type PostsInThreadModel from '@typings/database/models/servers/posts_in_thread'; -const {SERVER: {POST, POSTS_IN_CHANNEL, POSTS_IN_THREAD}} = MM_TABLES; +const {SERVER: {CHANNEL, POST, POSTS_IN_CHANNEL, POSTS_IN_THREAD}} = MM_TABLES; export const prepareDeletePost = async (post: PostModel): Promise => { const preparedModels: Model[] = [post.prepareDestroyPermanently()]; @@ -178,3 +178,30 @@ export const queryPostsBetween = (database: Database, earliest: number, latest: } return database.get(POST).query(...clauses); }; + +export const observeLastPostAtPerChannelByTeam = (database: Database, teamId: string) => { + return database.get(POSTS_IN_CHANNEL).query( + Q.on(CHANNEL, + Q.and( + Q.or( + Q.where('team_id', Q.eq(teamId)), + Q.where('team_id', Q.eq('')), + ), + Q.where('delete_at', Q.eq(0)), + ), + ), + Q.sortBy('latest', Q.desc), + ).observeWithColumns(['latest']).pipe( + switchMap((pics) => { + return of$(pics.reduce>((result, pic) => { + const id = pic.channelId; + if (result[id]) { + result[id] = Math.max(result[id], pic.latest); + } else { + result[id] = pic.latest; + } + return result; + }, {})); + }), + ); +}; diff --git a/app/screens/home/channel_list/categories_list/categories/unreads/index.ts b/app/screens/home/channel_list/categories_list/categories/unreads/index.ts index 5973b4dc7..163819b74 100644 --- a/app/screens/home/channel_list/categories_list/categories/unreads/index.ts +++ b/app/screens/home/channel_list/categories_list/categories/unreads/index.ts @@ -9,8 +9,10 @@ import {combineLatestWith, concatAll, map, switchMap} from 'rxjs/operators'; import {Preferences} from '@constants'; import {getPreferenceAsBool} from '@helpers/api/preference'; import {getChannelById, observeAllMyChannelNotifyProps, queryMyChannelUnreads} from '@queries/servers/channel'; +import {observeLastPostAtPerChannelByTeam} from '@queries/servers/post'; import {queryPreferencesByCategoryAndName} from '@queries/servers/preference'; import {observeLastUnreadChannelId} from '@queries/servers/system'; +import {observeIsCRTEnabled} from '@queries/servers/thread'; import {getChannelsFromRelation} from '../body'; @@ -40,7 +42,10 @@ type NotifyProps = { [key: string]: Partial; } -const mostRecentFirst = (a: MyChannelModel, b: MyChannelModel) => { +const mostRecentFirst = (lastPostAtPerChannel: Record | undefined, a: MyChannelModel, b: MyChannelModel) => { + if (lastPostAtPerChannel && lastPostAtPerChannel[a.id] && lastPostAtPerChannel[b.id]) { + return lastPostAtPerChannel[b.id] - lastPostAtPerChannel[a.id]; + } return b.lastPostAt - a.lastPostAt; }; @@ -50,10 +55,16 @@ const mostRecentFirst = (a: MyChannelModel, b: MyChannelModel) => { * Unreads, Mentions, and Muted Mentions Only * * Mentions on top, then unreads, then muted channels with mentions. - * Secondary sorting within each of those is by recent posting. + * Secondary sorting within each of those is by recent posting or by recent root post if CRT is enabled. */ -const filterAndSortMyChannels = ([myChannels, notifyProps]: [MyChannelModel[], NotifyProps]): MyChannelModel[] => { +type FilterAndSortMyChannelsArgs = [ + MyChannelModel[], + NotifyProps, + Record | undefined +] + +const filterAndSortMyChannels = ([myChannels, notifyProps, lastPostAtPerChannel]: FilterAndSortMyChannelsArgs): MyChannelModel[] => { const mentions: MyChannelModel[] = []; const unreads: MyChannelModel[] = []; const mutedMentions: MyChannelModel[] = []; @@ -63,8 +74,10 @@ const filterAndSortMyChannels = ([myChannels, notifyProps]: [MyChannelModel[], N }; for (const myChannel of myChannels) { + const id = myChannel.id; + // is it a mention? - if (!isMuted(myChannel.id) && myChannel.mentionsCount > 0) { + if (!isMuted(id) && myChannel.mentionsCount > 0) { mentions.push(myChannel); continue; } @@ -83,9 +96,9 @@ const filterAndSortMyChannels = ([myChannels, notifyProps]: [MyChannelModel[], N } // Sort - mentions.sort(mostRecentFirst); - unreads.sort(mostRecentFirst); - mutedMentions.sort(mostRecentFirst); + mentions.sort(mostRecentFirst.bind(null, lastPostAtPerChannel)); + unreads.sort(mostRecentFirst.bind(null, lastPostAtPerChannel)); + mutedMentions.sort(mostRecentFirst.bind(null, lastPostAtPerChannel)); return [...mentions, ...unreads, ...mutedMentions]; }; @@ -105,9 +118,13 @@ const enhanced = withObservables(['currentTeamId', 'isTablet', 'onlyUnreads'], ( switchMap(getC), ) : of$(''); const notifyProps = observeAllMyChannelNotifyProps(database); + const crt = observeIsCRTEnabled(database); + const lastPostInChannel = crt.pipe( + switchMap((enabled) => (enabled && onlyUnreads ? observeLastPostAtPerChannelByTeam(database, currentTeamId) : of$(undefined))), + ); - const unreads = queryMyChannelUnreads(database, currentTeamId).observe().pipe( - combineLatestWith(notifyProps), + const unreads = queryMyChannelUnreads(database, currentTeamId).observeWithColumns(['last_post_at']).pipe( + combineLatestWith(notifyProps, lastPostInChannel), map(filterAndSortMyChannels), map(getChannelsFromRelation), concatAll(), diff --git a/app/screens/home/channel_list/categories_list/threads_button/__snapshots__/threads_button.test.tsx.snap b/app/screens/home/channel_list/categories_list/threads_button/__snapshots__/threads_button.test.tsx.snap index 053fbab71..549ce4437 100644 --- a/app/screens/home/channel_list/categories_list/threads_button/__snapshots__/threads_button.test.tsx.snap +++ b/app/screens/home/channel_list/categories_list/threads_button/__snapshots__/threads_button.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Threads Component should match snapshot 1`] = ` +exports[`Thread item in the channel list Threads Component should match snapshot 1`] = ` `; + +exports[`Thread item in the channel list Threads Component should match snapshot with only unreads filter 1`] = `null`; diff --git a/app/screens/home/channel_list/categories_list/threads_button/index.ts b/app/screens/home/channel_list/categories_list/threads_button/index.ts index 912d535f0..cd0bcec09 100644 --- a/app/screens/home/channel_list/categories_list/threads_button/index.ts +++ b/app/screens/home/channel_list/categories_list/threads_button/index.ts @@ -5,7 +5,7 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import withObservables from '@nozbe/with-observables'; import {switchMap} from 'rxjs/operators'; -import {observeCurrentChannelId, observeCurrentTeamId} from '@queries/servers/system'; +import {observeCurrentChannelId, observeCurrentTeamId, observeOnlyUnreads} from '@queries/servers/system'; import {observeUnreadsAndMentionsInTeam} from '@queries/servers/thread'; import ThreadsButton from './threads_button'; @@ -17,6 +17,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => { return { currentChannelId: observeCurrentChannelId(database), + onlyUnreads: observeOnlyUnreads(database), unreadsAndMentions: currentTeamId.pipe( switchMap( (teamId) => observeUnreadsAndMentionsInTeam(database, teamId), diff --git a/app/screens/home/channel_list/categories_list/threads_button/threads_button.test.tsx b/app/screens/home/channel_list/categories_list/threads_button/threads_button.test.tsx index 25a796121..d71165916 100644 --- a/app/screens/home/channel_list/categories_list/threads_button/threads_button.test.tsx +++ b/app/screens/home/channel_list/categories_list/threads_button/threads_button.test.tsx @@ -7,16 +7,34 @@ import {renderWithIntlAndTheme} from '@test/intl-test-helper'; import Threads from './threads_button'; -test('Threads Component should match snapshot', () => { - const {toJSON} = renderWithIntlAndTheme( - , - ); +describe('Thread item in the channel list', () => { + test('Threads Component should match snapshot', () => { + const {toJSON} = renderWithIntlAndTheme( + , + ); - expect(toJSON()).toMatchSnapshot(); + expect(toJSON()).toMatchSnapshot(); + }); + + test('Threads Component should match snapshot with only unreads filter', () => { + const {toJSON} = renderWithIntlAndTheme( + , + ); + + expect(toJSON()).toMatchSnapshot(); + }); }); diff --git a/app/screens/home/channel_list/categories_list/threads_button/threads_button.tsx b/app/screens/home/channel_list/categories_list/threads_button/threads_button.tsx index 4d314dd0c..7131dd710 100644 --- a/app/screens/home/channel_list/categories_list/threads_button/threads_button.tsx +++ b/app/screens/home/channel_list/categories_list/threads_button/threads_button.tsx @@ -37,13 +37,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ type Props = { currentChannelId: string; + onlyUnreads: boolean; unreadsAndMentions: { unreads: number; mentions: number; }; }; -const ThreadsButton = ({currentChannelId, unreadsAndMentions}: Props) => { +const ThreadsButton = ({currentChannelId, onlyUnreads, unreadsAndMentions}: Props) => { const isTablet = useIsTablet(); const serverUrl = useServerUrl(); @@ -71,7 +72,7 @@ const ThreadsButton = ({currentChannelId, unreadsAndMentions}: Props) => { const text = [ customStyles.text, - unreads ? channelItemTextStyle.bright : channelItemTextStyle.regular, + unreads ? channelItemTextStyle.bold : channelItemTextStyle.regular, styles.text, unreads ? styles.highlight : undefined, isActive ? styles.textActive : undefined, @@ -80,6 +81,10 @@ const ThreadsButton = ({currentChannelId, unreadsAndMentions}: Props) => { return [container, icon, text]; }, [customStyles, isActive, styles, unreads]); + if (onlyUnreads && !unreads && !mentions) { + return null; + } + return (