From 10ea42c8127b35563389542735900bb114f9b65d Mon Sep 17 00:00:00 2001 From: Shaz MJ Date: Wed, 13 Apr 2022 23:36:47 +1000 Subject: [PATCH] Active State for selected Channel (#6163) * Unreads on top * Feedback addressed * WIP: Using a lastUnreadChannelId to maintain an Active State on selected channel * Pair programming session * Pair programming #2 * Pair programming #3 * Test fix --- app/actions/local/channel.test.ts | 22 ++++++------ app/actions/local/channel.ts | 19 +++++----- app/actions/remote/entry/app.ts | 7 ++++ app/actions/remote/team.ts | 2 +- app/actions/websocket/index.ts | 13 +++++-- app/actions/websocket/posts.ts | 14 ++++++-- app/components/channel_icon/index.tsx | 6 ++-- .../__snapshots__/category_body.test.tsx.snap | 16 +++++---- .../channel_list_item.test.tsx.snap | 16 +++++---- .../body/channel/channel_list_item.tsx | 32 ++++++++++++----- .../channel_list/categories/categories.tsx | 9 ++++- .../header/__snapshots__/header.test.tsx.snap | 1 + .../channel_list/categories/header/header.tsx | 1 + .../channel_list/categories/index.ts | 29 ++++++++++++--- .../channel_list/categories/unreads.test.tsx | 5 ++- .../channel_list/categories/unreads.tsx | 32 ++++++++++------- app/components/channel_list/index.tsx | 1 - app/constants/database.ts | 1 + app/queries/servers/system.ts | 35 ++++++++++++++++++- .../home/channel_list/channel_list.tsx | 17 +++------ 20 files changed, 195 insertions(+), 83 deletions(-) diff --git a/app/actions/local/channel.test.ts b/app/actions/local/channel.test.ts index c74c79102..c176fb0e5 100644 --- a/app/actions/local/channel.test.ts +++ b/app/actions/local/channel.test.ts @@ -121,7 +121,7 @@ describe('switchToChannel', () => { const {systemValues, teamHistory, channelHistory, member} = await queryDatabaseValues(operator.database, teamId, channelId); expect(error).toBeUndefined(); - expect(models?.length).toBe(1); // Viewed at + expect(models?.length).toBe(2); // Viewed at, lastUnread expect(systemValues.currentTeamId).toBe(teamId); expect(systemValues.currentChannelId).toBe(channelId); expect(teamHistory.length).toBe(0); @@ -146,7 +146,7 @@ describe('switchToChannel', () => { const {systemValues, teamHistory, channelHistory, member} = await queryDatabaseValues(operator.database, teamId, channelId); expect(error).toBeUndefined(); - expect(models?.length).toBe(3); // Viewed at, channel history and currentChannelId + expect(models?.length).toBe(4); // Viewed at, channel history and currentChannelId expect(systemValues.currentTeamId).toBe(teamId); expect(systemValues.currentChannelId).toBe(channelId); expect(teamHistory.length).toBe(0); @@ -172,7 +172,7 @@ describe('switchToChannel', () => { const {systemValues, teamHistory, channelHistory, member} = await queryDatabaseValues(operator.database, teamId, channelId); expect(error).toBeUndefined(); - expect(models?.length).toBe(5); // Viewed at, channel history, team history, currentTeamId and currentChannelId + expect(models?.length).toBe(6); // Viewed at, channel history, team history, currentTeamId and currentChannelId expect(systemValues.currentTeamId).toBe(teamId); expect(systemValues.currentChannelId).toBe(channelId); expect(teamHistory.length).toBe(1); @@ -200,7 +200,7 @@ describe('switchToChannel', () => { const {systemValues, teamHistory, channelHistory, member} = await queryDatabaseValues(operator.database, teamId, channelId); expect(error).toBeUndefined(); - expect(models?.length).toBe(4); // Viewed at, channel history, team history and currentTeamId + expect(models?.length).toBe(5); // Viewed at, channel history, team history and currentTeamId, lastUnread expect(systemValues.currentTeamId).toBe(teamId); expect(systemValues.currentChannelId).toBe(channelId); expect(teamHistory.length).toBe(1); @@ -228,7 +228,7 @@ describe('switchToChannel', () => { const {systemValues, teamHistory, channelHistory, member} = await queryDatabaseValues(operator.database, teamId, channelId); expect(error).toBeUndefined(); - expect(models?.length).toBe(5); // Viewed at, channel history, team history, currentTeamId and currentChannelId + expect(models?.length).toBe(6); // Viewed at, channel history, team history, currentTeamId and currentChannelId, lastUnread expect(systemValues.currentTeamId).toBe(teamId); expect(systemValues.currentChannelId).toBe(channelId); expect(teamHistory.length).toBe(1); @@ -255,7 +255,7 @@ describe('switchToChannel', () => { const {systemValues, teamHistory, channelHistory, member} = await queryDatabaseValues(operator.database, teamId, channelId); expect(error).toBeUndefined(); - expect(models?.length).toBe(5); // Viewed at, channel history, team history, currentTeamId and currentChannelId + expect(models?.length).toBe(6); // Viewed at, channel history, team history, currentTeamId and currentChannelId, lastUnread expect(systemValues.currentTeamId).toBe(teamId); expect(systemValues.currentChannelId).toBe(channelId); expect(teamHistory.length).toBe(1); @@ -282,7 +282,7 @@ describe('switchToChannel', () => { const {systemValues, teamHistory, channelHistory, member} = await queryDatabaseValues(operator.database, teamId, channelId); expect(error).toBeUndefined(); - expect(models?.length).toBe(3); // Viewed at, channel history and currentChannelId + expect(models?.length).toBe(4); // Viewed at, channel history and currentChannelId, lastUnread expect(systemValues.currentTeamId).toBe(teamId); expect(systemValues.currentChannelId).toBe(channelId); expect(teamHistory.length).toBe(0); @@ -309,7 +309,7 @@ describe('switchToChannel', () => { const {systemValues, teamHistory, channelHistory, member} = await queryDatabaseValues(operator.database, teamId, channelId); expect(error).toBeUndefined(); - expect(models?.length).toBe(3); // Viewed at, channel history and currentChannelId + expect(models?.length).toBe(4); // Viewed at, channel history and currentChannelId, lastUnread expect(systemValues.currentTeamId).toBe(teamId); expect(systemValues.currentChannelId).toBe(channelId); expect(teamHistory.length).toBe(0); @@ -335,7 +335,7 @@ describe('switchToChannel', () => { const {systemValues, teamHistory, channelHistory, member} = await queryDatabaseValues(operator.database, teamId, channelId); expect(error).toBeUndefined(); - expect(models?.length).toBe(5); // Viewed at, channel history, team history, currentTeamId and currentChannelId + expect(models?.length).toBe(6); // Viewed at, channel history, team history, currentTeamId and currentChannelId, lastUnread expect(systemValues.currentTeamId).toBe(teamId); expect(systemValues.currentChannelId).toBe(channelId); expect(teamHistory.length).toBe(1); @@ -395,7 +395,7 @@ describe('switchToChannel', () => { const {systemValues, teamHistory, channelHistory, member} = await queryDatabaseValues(operator.database, teamId, channelId); expect(error).toBeUndefined(); - expect(models?.length).toBe(5); // Viewed at, channel history, team history, currentTeamId and currentChannelId + expect(models?.length).toBe(6); // Viewed at, channel history, team history, currentTeamId and currentChannelId, lastunread expect(systemValues.currentTeamId).toBe(currentTeamId); expect(systemValues.currentChannelId).toBe(currentChannelId); expect(teamHistory.length).toBe(0); @@ -422,7 +422,7 @@ describe('switchToChannel', () => { const {systemValues, teamHistory, channelHistory, member} = await queryDatabaseValues(operator.database, teamId, channelId); expect(error).toBeUndefined(); - expect(models?.length).toBe(5); // Viewed at, channel history, team history, currentTeamId and currentChannelId + expect(models?.length).toBe(6); // Viewed at, channel history, team history, currentTeamId and currentChannelId, lastUnread expect(systemValues.currentTeamId).toBe(teamId); expect(systemValues.currentChannelId).toBe(channelId); expect(teamHistory.length).toBe(1); diff --git a/app/actions/local/channel.ts b/app/actions/local/channel.ts index f05fdfd22..89e3c72c6 100644 --- a/app/actions/local/channel.ts +++ b/app/actions/local/channel.ts @@ -61,15 +61,18 @@ export async function switchToChannel(serverUrl: string, channelId: string, team models.push(...history); } + const commonValues: PrepareCommonSystemValuesArgs = { + lastUnreadChannelId: member.isUnread ? channelId : '', + }; + if ((system.currentTeamId !== toTeamId) || (system.currentChannelId !== channelId)) { - const commonValues: PrepareCommonSystemValuesArgs = { - currentChannelId: system.currentChannelId === channelId ? undefined : channelId, - currentTeamId: system.currentTeamId === toTeamId ? undefined : toTeamId, - }; - const common = await prepareCommonSystemValues(operator, commonValues); - if (common) { - models.push(...common); - } + commonValues.currentChannelId = system.currentChannelId === channelId ? undefined : channelId; + commonValues.currentTeamId = system.currentTeamId === toTeamId ? undefined : toTeamId; + } + + const common = await prepareCommonSystemValues(operator, commonValues); + if (common) { + models.push(...common); } if (system.currentChannelId !== channelId || system.currentTeamId !== toTeamId) { diff --git a/app/actions/remote/entry/app.ts b/app/actions/remote/entry/app.ts index 7b41c35bc..021a2994b 100644 --- a/app/actions/remote/entry/app.ts +++ b/app/actions/remote/entry/app.ts @@ -21,6 +21,12 @@ export async function appEntry(serverUrl: string, since = 0) { } const {database} = operator; + // clear lastUnreadChannelId + const removeLastUnreadChannelId = await prepareCommonSystemValues(operator, {lastUnreadChannelId: ''}); + if (removeLastUnreadChannelId) { + operator.batchRecords(removeLastUnreadChannelId); + } + const tabletDevice = await isTablet(); const currentTeamId = await getCurrentTeamId(database); const lastDisconnectedAt = (await getWebSocketLastDisconnected(database)) || since; @@ -72,6 +78,7 @@ export async function appEntry(serverUrl: string, since = 0) { if (rolesData.roles?.length) { modelPromises.push(operator.handleRole({roles: rolesData.roles, prepareRecordsOnly: true})); } + const models = await Promise.all(modelPromises); await operator.batchRecords(models.flat()); diff --git a/app/actions/remote/team.ts b/app/actions/remote/team.ts index ecbfb6e36..7c50027c6 100644 --- a/app/actions/remote/team.ts +++ b/app/actions/remote/team.ts @@ -274,7 +274,7 @@ export async function handleTeamChange(serverUrl: string, teamId: string) { } const models = []; - const system = await prepareCommonSystemValues(operator, {currentChannelId: channelId, currentTeamId: teamId}); + const system = await prepareCommonSystemValues(operator, {currentChannelId: channelId, currentTeamId: teamId, lastUnreadChannelId: ''}); if (system?.length) { models.push(...system); } diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts index 850c09a74..1d973e265 100644 --- a/app/actions/websocket/index.ts +++ b/app/actions/websocket/index.ts @@ -1,7 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {fetchMissingSidebarInfo, switchToChannelById} from '@actions/remote/channel'; +import {markChannelAsViewed} from '@actions/local/channel'; +import {fetchMissingSidebarInfo, markChannelAsRead, switchToChannelById} from '@actions/remote/channel'; import {AppEntryData, AppEntryError, fetchAppEntryData, teamsToRemove} from '@actions/remote/entry/common'; import {fetchPostsForUnreadChannels, fetchPostsSince} from '@actions/remote/post'; import {fetchRoles} from '@actions/remote/role'; @@ -9,13 +10,14 @@ import {fetchConfigAndLicense} from '@actions/remote/systems'; import {fetchAllTeams, fetchTeamsChannelsAndUnreadPosts} from '@actions/remote/team'; import {fetchNewThreads} from '@actions/remote/thread'; import {fetchStatusByIds, updateAllUsersSince} from '@actions/remote/user'; -import {WebsocketEvents} from '@constants'; +import {Screens, WebsocketEvents} from '@constants'; import {SYSTEM_IDENTIFIERS} from '@constants/database'; import DatabaseManager from '@database/manager'; import {getTeammateNameDisplaySetting} from '@helpers/api/preference'; import {queryChannelsById, getDefaultChannelForTeam} from '@queries/servers/channel'; import {prepareModels} from '@queries/servers/entry'; import {getCommonSystemValues, getConfig, getCurrentChannelId, getWebSocketLastDisconnected, resetWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system'; +import EphemeralStore from '@store/ephemeral_store'; import {isDMorGM} from '@utils/channel'; import {isTablet} from '@utils/helpers'; import {isCRTEnabled} from '@utils/thread'; @@ -188,6 +190,13 @@ async function doReconnect(serverUrl: string) { // https://mattermost.atlassian.net/browse/MM-40098 fetchPostsSince(serverUrl, currentChannelId, lastDisconnectedAt); + const isChannelScreenMounted = EphemeralStore.getNavigationComponents().includes(Screens.CHANNEL); + + if (isChannelScreenMounted || tabletDevice) { + markChannelAsRead(serverUrl, currentChannelId); + markChannelAsViewed(serverUrl, currentChannelId); + } + // defer fetching posts for unread channels on initial team if (chData?.channels && chData.memberships) { fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, currentChannelId); diff --git a/app/actions/websocket/posts.ts b/app/actions/websocket/posts.ts index 8a2b161e2..28b516793 100644 --- a/app/actions/websocket/posts.ts +++ b/app/actions/websocket/posts.ts @@ -10,12 +10,14 @@ import {createThreadFromNewPost, updateThread} from '@actions/local/thread'; import {fetchMyChannel, markChannelAsRead} from '@actions/remote/channel'; import {fetchPostAuthors, fetchPostById} from '@actions/remote/post'; import {fetchThread} from '@actions/remote/thread'; -import {ActionType, Events} from '@constants'; +import {ActionType, Events, Screens} from '@constants'; import DatabaseManager from '@database/manager'; import {getChannelById, getMyChannel} from '@queries/servers/channel'; import {getPostById} from '@queries/servers/post'; import {getCurrentChannelId, getCurrentUserId} from '@queries/servers/system'; import {getIsCRTEnabled} from '@queries/servers/thread'; +import EphemeralStore from '@store/ephemeral_store'; +import {isTablet} from '@utils/helpers'; import {isFromWebhook, isSystemMessage, shouldIgnorePost} from '@utils/post'; import type MyChannelModel from '@typings/database/models/servers/my_channel'; @@ -136,8 +138,14 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag } else if ((post.channel_id === currentChannelId)) { // TODO: THREADS && !viewingGlobalThreads) { // Don't mark as read if we're in global threads screen // the currentChannelId still refers to previously viewed channel - markAsViewed = false; - markAsRead = true; + + const isChannelScreenMounted = EphemeralStore.getNavigationComponents().includes(Screens.CHANNEL); + + const isTabletDevice = await isTablet(); + if (isChannelScreenMounted || isTabletDevice) { + markAsViewed = false; + markAsRead = true; + } } } diff --git a/app/components/channel_icon/index.tsx b/app/components/channel_icon/index.tsx index 04aaed4eb..c7929ad9b 100644 --- a/app/components/channel_icon/index.tsx +++ b/app/components/channel_icon/index.tsx @@ -38,7 +38,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { color: changeOpacity(theme.sidebarText, 0.4), }, iconActive: { - color: theme.sidebarTextActiveColor, + color: theme.sidebarText, }, iconUnread: { color: theme.sidebarUnreadText, @@ -53,7 +53,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { justifyContent: 'center', }, groupBoxActive: { - backgroundColor: changeOpacity(theme.sidebarTextActiveColor, 0.3), + backgroundColor: changeOpacity(theme.sidebarText, 0.3), }, groupBoxUnread: { backgroundColor: changeOpacity(theme.sidebarUnreadText, 0.3), @@ -66,7 +66,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { ...typography('Body', 75, 'SemiBold'), }, groupActive: { - color: theme.sidebarTextActiveColor, + color: theme.sidebarText, }, groupUnread: { color: theme.sidebarUnreadText, diff --git a/app/components/channel_list/categories/body/__snapshots__/category_body.test.tsx.snap b/app/components/channel_list/categories/body/__snapshots__/category_body.test.tsx.snap index 2ef2e9937..2b0959a90 100644 --- a/app/components/channel_list/categories/body/__snapshots__/category_body.test.tsx.snap +++ b/app/components/channel_list/categories/body/__snapshots__/category_body.test.tsx.snap @@ -46,12 +46,15 @@ Object { > @@ -108,6 +111,7 @@ Object { }, false, false, + null, ] } testID="category.test_category.channel_list_item.channel.display_name" diff --git a/app/components/channel_list/categories/body/channel/__snapshots__/channel_list_item.test.tsx.snap b/app/components/channel_list/categories/body/channel/__snapshots__/channel_list_item.test.tsx.snap index 25cd8f94c..3a8e3db30 100644 --- a/app/components/channel_list/categories/body/channel/__snapshots__/channel_list_item.test.tsx.snap +++ b/app/components/channel_list/categories/body/channel/__snapshots__/channel_list_item.test.tsx.snap @@ -39,12 +39,15 @@ exports[`components/channel_list/categories/body/channel/item should match snaps > @@ -124,6 +127,7 @@ exports[`components/channel_list/categories/body/channel/item should match snaps }, false, false, + null, ] } testID="channel_list_item.hello.display_name" diff --git a/app/components/channel_list/categories/body/channel/channel_list_item.tsx b/app/components/channel_list/categories/body/channel/channel_list_item.tsx index df9ab080d..992b4919e 100644 --- a/app/components/channel_list/categories/body/channel/channel_list_item.tsx +++ b/app/components/channel_list/categories/body/channel/channel_list_item.tsx @@ -22,7 +22,7 @@ import type MyChannelModel from '@typings/database/models/servers/my_channel'; const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ container: { flexDirection: 'row', - paddingLeft: 2, + paddingHorizontal: 20, height: 40, alignItems: 'center', }, @@ -52,6 +52,17 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ mutedBadge: { opacity: 0.4, }, + activeItem: { + backgroundColor: changeOpacity(theme.sidebarTextActiveColor, 0.1), + borderLeftColor: theme.sidebarTextActiveBorder, + borderLeftWidth: 5, + marginLeft: 0, + paddingLeft: 14, + }, + textActive: { + color: theme.sidebarText, + }, + })); const textStyle = StyleSheet.create({ @@ -78,13 +89,14 @@ const ChannelListItem = ({channel, isActive, currentUserId, isMuted, myChannel, const isOwnDirectMessage = (channel.type === General.DM_CHANNEL) && currentUserId === getUserIdFromChannelName(currentUserId, channel.name); // Make it brighter if it's not muted, and highlighted or has unreads - const bright = !isMuted && (isActive || (myChannel && (myChannel.isUnread || myChannel.mentionsCount > 0))); + const isUnread = !isMuted && (myChannel && (myChannel.isUnread || myChannel.mentionsCount > 0)); - const sharedValue = useSharedValue(collapsed && !bright); + const shouldCollapse = (collapsed && !isUnread) && !isActive; + const sharedValue = useSharedValue(shouldCollapse); useEffect(() => { - sharedValue.value = collapsed && !bright; - }, [collapsed && !bright]); + sharedValue.value = shouldCollapse; + }, [shouldCollapse]); const animatedStyle = useAnimatedStyle(() => { return { @@ -99,6 +111,7 @@ const ChannelListItem = ({channel, isActive, currentUserId, isMuted, myChannel, switchToChannelById(serverUrl, myChannel.id); } }, [myChannel?.id, serverUrl]); + const membersCount = useMemo(() => { if (channel.type === General.GM_CHANNEL) { return channel.displayName.split(',').length; @@ -107,11 +120,12 @@ const ChannelListItem = ({channel, isActive, currentUserId, isMuted, myChannel, }, [channel.type, channel.displayName]); const textStyles = useMemo(() => [ - bright ? textStyle.bright : textStyle.regular, + isUnread ? textStyle.bright : textStyle.regular, styles.text, - bright && styles.highlight, + isUnread && styles.highlight, isMuted && styles.muted, - ], [bright, styles, isMuted]); + isActive ? styles.textActive : null, + ], [isUnread, styles, isMuted]); let displayName = channel.displayName; if (isOwnDirectMessage) { @@ -126,7 +140,7 @@ const ChannelListItem = ({channel, isActive, currentUserId, isMuted, myChannel, { if (Array.isArray(data.item)) { - return ; + return ( + + ); } return ( diff --git a/app/components/channel_list/categories/header/__snapshots__/header.test.tsx.snap b/app/components/channel_list/categories/header/__snapshots__/header.test.tsx.snap index f07dbb110..03eebdbd9 100644 --- a/app/components/channel_list/categories/header/__snapshots__/header.test.tsx.snap +++ b/app/components/channel_list/categories/header/__snapshots__/header.test.tsx.snap @@ -24,6 +24,7 @@ exports[`components/channel_list/categories/header should match snapshot 1`] = ` Object { "alignItems": "flex-start", "flexDirection": "row", + "marginLeft": 16, "marginTop": 12, "paddingLeft": 2, "paddingVertical": 8, diff --git a/app/components/channel_list/categories/header/header.tsx b/app/components/channel_list/categories/header/header.tsx index 378d276c9..51fdce334 100644 --- a/app/components/channel_list/categories/header/header.tsx +++ b/app/components/channel_list/categories/header/header.tsx @@ -19,6 +19,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ paddingVertical: 8, marginTop: 12, paddingLeft: 2, + marginLeft: 16, flexDirection: 'row', alignItems: 'flex-start', }, diff --git a/app/components/channel_list/categories/index.ts b/app/components/channel_list/categories/index.ts index 4a7a303b1..3e3098533 100644 --- a/app/components/channel_list/categories/index.ts +++ b/app/components/channel_list/categories/index.ts @@ -3,22 +3,31 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import withObservables from '@nozbe/with-observables'; -import {of as of$} from 'rxjs'; +import {combineLatest, of as of$} from 'rxjs'; import {concatAll, map, switchMap} from 'rxjs/operators'; import {Preferences} from '@app/constants'; import {getPreferenceAsBool} from '@app/helpers/api/preference'; -import {queryMyChannelUnreads} from '@app/queries/servers/channel'; +import {getChannelById, queryMyChannelUnreads} from '@app/queries/servers/channel'; import {queryPreferencesByCategoryAndName} from '@app/queries/servers/preference'; import {queryCategoriesByTeamIds} from '@queries/servers/categories'; -import {observeCurrentChannelId, observeCurrentUserId} from '@queries/servers/system'; +import {observeCurrentChannelId, observeCurrentUserId, observeLastUnreadChannelId} from '@queries/servers/system'; import {getChannelsFromRelation} from './body'; import Categories from './categories'; import type {WithDatabaseArgs} from '@typings/database/database'; +import type ChannelModel from '@typings/database/models/servers/channel'; import type PreferenceModel from '@typings/database/models/servers/preference'; +type CA = [ + a: Array, + b: ChannelModel | undefined, +] +const concatenateChannelsArray = ([a, b]: CA) => { + return of$(b ? a.filter((c) => c && c.id !== b.id).concat(b) : a); +}; + type WithDatabaseProps = { currentTeamId: string } & WithDatabaseArgs const enhanced = withObservables( @@ -34,12 +43,24 @@ const enhanced = withObservables( switchMap((prefs: PreferenceModel[]) => of$(getPreferenceAsBool(prefs, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS, false))), ); + const getC = (lastUnreadChannelId: string) => getChannelById(database, lastUnreadChannelId); + const unreadChannels = unreadsOnTop.pipe(switchMap((gU) => { if (gU) { - return queryMyChannelUnreads(database, currentTeamId).observe().pipe( + const lastUnread = observeLastUnreadChannelId(database).pipe( + switchMap(getC), + ); + + const unreads = queryMyChannelUnreads(database, currentTeamId).observe().pipe( map(getChannelsFromRelation), concatAll(), ); + + const combined = combineLatest([unreads, lastUnread]).pipe( + switchMap(concatenateChannelsArray), + ); + + return combined; } return of$([]); })); diff --git a/app/components/channel_list/categories/unreads.test.tsx b/app/components/channel_list/categories/unreads.test.tsx index a415836d1..14eb1f3e9 100644 --- a/app/components/channel_list/categories/unreads.test.tsx +++ b/app/components/channel_list/categories/unreads.test.tsx @@ -19,7 +19,10 @@ describe('components/channel_list/categories/body', () => { it('render without error', () => { const wrapper = renderWithEverything( - , + , {database}, ); diff --git a/app/components/channel_list/categories/unreads.tsx b/app/components/channel_list/categories/unreads.tsx index 6d132bd52..0a40d14ee 100644 --- a/app/components/channel_list/categories/unreads.tsx +++ b/app/components/channel_list/categories/unreads.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React from 'react'; +import React, {useCallback} from 'react'; import {useIntl} from 'react-intl'; import {FlatList, Text} from 'react-native'; @@ -17,26 +17,32 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ heading: { color: changeOpacity(theme.sidebarText, 0.64), ...typography('Heading', 75), - paddingLeft: 5, - paddingTop: 10, + paddingLeft: 18, + paddingVertical: 8, + marginTop: 12, }, })); -const renderItem = ({item}: {item: ChannelModel}) => { - return ( - - ); -}; +type UnreadCategoriesProps = { + unreadChannels: ChannelModel[]; + currentChannelId: string; +} -const UnreadCategories = ({unreadChannels}: {unreadChannels: ChannelModel[]}) => { +const UnreadCategories = ({unreadChannels, currentChannelId}: UnreadCategoriesProps) => { const theme = useTheme(); const styles = getStyleSheet(theme); const intl = useIntl(); + const renderItem = useCallback(({item}: {item: ChannelModel}) => { + return ( + + ); + }, [currentChannelId]); + return ( <> ({ paddingRight: 20, paddingTop: 10, }, - })); type ChannelListProps = { diff --git a/app/constants/database.ts b/app/constants/database.ts index 7711e07fc..aa6bd1551 100644 --- a/app/constants/database.ts +++ b/app/constants/database.ts @@ -48,6 +48,7 @@ export const MIGRATION_EVENTS = keyMirror({ export const SYSTEM_IDENTIFIERS = { CONFIG: 'config', CURRENT_CHANNEL_ID: 'currentChannelId', + LAST_UNREAD_CHANNEL_ID: 'lastUnreadChannelId', CURRENT_TEAM_ID: 'currentTeamId', CURRENT_USER_ID: 'currentUserId', DATA_RETENTION_POLICIES: 'dataRetentionPolicies', diff --git a/app/queries/servers/system.ts b/app/queries/servers/system.ts index 3a7dd3eea..7cbca90a5 100644 --- a/app/queries/servers/system.ts +++ b/app/queries/servers/system.ts @@ -12,6 +12,7 @@ import type SystemModel from '@typings/database/models/servers/system'; export type PrepareCommonSystemValuesArgs = { config?: ClientConfig; + lastUnreadChannelId?: string; currentChannelId?: string; currentTeamId?: string; currentUserId?: string; @@ -83,6 +84,7 @@ export const getCommonSystemValues = async (serverDatabase: Database) => { let currentChannelId = ''; let currentTeamId = ''; let currentUserId = ''; + let lastUnreadChannelId = ''; systemRecords.forEach((systemRecord) => { switch (systemRecord.id) { case SYSTEM_IDENTIFIERS.CONFIG: @@ -100,6 +102,9 @@ export const getCommonSystemValues = async (serverDatabase: Database) => { case SYSTEM_IDENTIFIERS.LICENSE: license = systemRecord.value as ClientLicense; break; + case SYSTEM_IDENTIFIERS.LAST_UNREAD_CHANNEL_ID: + lastUnreadChannelId = systemRecord.value; + break; } }); @@ -107,6 +112,7 @@ export const getCommonSystemValues = async (serverDatabase: Database) => { currentChannelId, currentTeamId, currentUserId, + lastUnreadChannelId, config: (config as ClientConfig), license: (license as ClientLicense), }; @@ -274,7 +280,7 @@ export const patchTeamHistory = (operator: ServerDataOperator, value: string[], export async function prepareCommonSystemValues( operator: ServerDataOperator, values: PrepareCommonSystemValuesArgs): Promise { try { - const {config, currentChannelId, currentTeamId, currentUserId, license} = values; + const {config, lastUnreadChannelId, currentChannelId, currentTeamId, currentUserId, license} = values; const systems: IdValue[] = []; if (config !== undefined) { systems.push({ @@ -290,6 +296,13 @@ export async function prepareCommonSystemValues( }); } + if (lastUnreadChannelId !== undefined) { + systems.push({ + id: SYSTEM_IDENTIFIERS.LAST_UNREAD_CHANNEL_ID, + value: lastUnreadChannelId, + }); + } + if (currentUserId !== undefined) { systems.push({ id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, @@ -349,3 +362,23 @@ export async function setCurrentTeamAndChannelId(operator: ServerDataOperator, t } } +export const observeLastUnreadChannelId = (database: Database) => { + return querySystemValue(database, SYSTEM_IDENTIFIERS.LAST_UNREAD_CHANNEL_ID).observe().pipe( + switchMap((result) => (result.length ? result[0].observe() : of$({value: ''}))), + ).pipe( + switchMap((model) => of$(model.value as string)), + ); +}; + +export const queryLastUnreadChannelId = (database: Database) => { + return querySystemValue(database, SYSTEM_IDENTIFIERS.LAST_UNREAD_CHANNEL_ID); +}; + +export const getLastUnreadChannelId = async (serverDatabase: Database): Promise => { + try { + const lastUnreadChannelId = (await queryLastUnreadChannelId(serverDatabase).fetch())[0]; + return lastUnreadChannelId?.value || ''; + } catch { + return ''; + } +}; diff --git a/app/screens/home/channel_list/channel_list.tsx b/app/screens/home/channel_list/channel_list.tsx index 88051c985..8b19b70a6 100644 --- a/app/screens/home/channel_list/channel_list.tsx +++ b/app/screens/home/channel_list/channel_list.tsx @@ -4,6 +4,7 @@ import {useManagedConfig} from '@mattermost/react-native-emm'; import {useIsFocused, useRoute} from '@react-navigation/native'; import React from 'react'; +import {StyleSheet} from 'react-native'; import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated'; import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context'; @@ -13,7 +14,6 @@ import TeamSidebar from '@components/team_sidebar'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; import Channel from '@screens/channel'; -import {makeStyleSheetFromTheme} from '@utils/theme'; import Servers from './servers'; @@ -25,7 +25,8 @@ type ChannelProps = { }; const edges: Edge[] = ['bottom', 'left', 'right']; -const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ + +const styles = StyleSheet.create({ flex: { flex: 1, }, @@ -33,20 +34,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ flex: 1, flexDirection: 'row', }, - sectionContainer: { - marginTop: 32, - paddingHorizontal: 24, - }, - sectionTitle: { - fontSize: 24, - fontFamily: 'OpenSans-SemiBold', - color: theme.centerChannelColor, - }, -})); +}); const ChannelListScreen = (props: ChannelProps) => { const theme = useTheme(); - const styles = getStyleSheet(theme); const managedConfig = useManagedConfig(); const isTablet = useIsTablet();