diff --git a/app/actions/local/channel.ts b/app/actions/local/channel.ts index fc252bea75..5e6274544c 100644 --- a/app/actions/local/channel.ts +++ b/app/actions/local/channel.ts @@ -23,11 +23,12 @@ import type UserModel from '@typings/database/models/servers/user'; const {SERVER: {CHANNEL_MEMBERSHIP, USER}} = MM_TABLES; export const switchToChannel = async (serverUrl: string, channelId: string, teamId?: string, prepareRecordsOnly = false) => { - const database = DatabaseManager.serverDatabases[serverUrl]?.database; - if (!database) { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (!operator) { return {error: `${serverUrl} database not found`}; } + const {database} = operator; const models: Model[] = []; try { const dt = Date.now(); @@ -37,9 +38,11 @@ export const switchToChannel = async (serverUrl: string, channelId: string, team if (member) { const channel: ChannelModel = await member.channel.fetch(); - const {operator} = DatabaseManager.serverDatabases[serverUrl]; - const commonValues: PrepareCommonSystemValuesArgs = {currentChannelId: channelId}; - if (isTabletDevice) { + const commonValues: PrepareCommonSystemValuesArgs = {}; + if (system.currentChannelId !== channelId) { + commonValues.currentChannelId = channelId; + } + if (isTabletDevice && system.currentChannelId !== channelId) { // On tablet, the channel is being rendered, by setting the channel to empty first we speed up // the switch by ~3x await setCurrentChannelId(operator, ''); @@ -51,9 +54,11 @@ export const switchToChannel = async (serverUrl: string, channelId: string, team models.push(...history); } - const common = await prepareCommonSystemValues(operator, commonValues); - if (common) { - models.push(...common); + if (Object.keys(commonValues).length) { + const common = await prepareCommonSystemValues(operator, commonValues); + if (common) { + models.push(...common); + } } if (system.currentChannelId !== channelId) { diff --git a/app/actions/remote/entry/common.ts b/app/actions/remote/entry/common.ts index b64afe7945..0b2a788908 100644 --- a/app/actions/remote/entry/common.ts +++ b/app/actions/remote/entry/common.ts @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {fetchMissingSidebarInfo, fetchMyChannelsForTeam, MyChannelsRequest} from '@actions/remote/channel'; +import {fetchChannelStats, fetchMissingSidebarInfo, fetchMyChannelsForTeam, markChannelAsRead, MyChannelsRequest} from '@actions/remote/channel'; import {fetchGroupsForTeam} from '@actions/remote/group'; import {fetchPostsForChannel, fetchPostsForUnreadChannels} from '@actions/remote/post'; import {MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference'; @@ -164,6 +164,8 @@ export const deferredAppEntryActions = async ( // defer fetching posts for initial channel if (initialChannelId) { fetchPostsForChannel(serverUrl, initialChannelId); + markChannelAsRead(serverUrl, initialChannelId); + fetchChannelStats(serverUrl, initialChannelId); } // defer sidebar DM & GM profiles diff --git a/app/actions/remote/entry/login.ts b/app/actions/remote/entry/login.ts index b4c2092d59..e43ef77c35 100644 --- a/app/actions/remote/entry/login.ts +++ b/app/actions/remote/entry/login.ts @@ -5,7 +5,7 @@ import {Model} from '@nozbe/watermelondb'; import {fetchMyChannelsForTeam, MyChannelsRequest} from '@actions/remote/channel'; import {MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference'; -import {fetchRolesIfNeeded} from '@actions/remote/role'; +import {fetchRolesIfNeeded, RolesRequest} from '@actions/remote/role'; import {getSessions} from '@actions/remote/session'; import {ConfigAndLicenseRequest, fetchConfigAndLicense} from '@actions/remote/systems'; import {fetchMyTeams, MyTeamsRequest} from '@actions/remote/team'; @@ -16,8 +16,9 @@ import {selectDefaultTeam} from '@helpers/api/team'; import NetworkManager from '@init/network_manager'; import {prepareModels} from '@queries/servers/entry'; import {prepareCommonSystemValues} from '@queries/servers/system'; -import {addChannelToTeamHistory} from '@queries/servers/team'; +import {addChannelToTeamHistory, addTeamToTeamHistory} from '@queries/servers/team'; import {selectDefaultChannelForTeam} from '@utils/channel'; +import {isTablet} from '@utils/helpers'; import {scheduleExpiredNotification} from '@utils/notification'; import {deferredAppEntryActions} from './common'; @@ -53,6 +54,7 @@ export const loginEntry = async ({serverUrl, user, deviceToken}: AfterLoginArgs) } try { + const isTabletDevice = await isTablet(); let initialTeam: Team|undefined; let initialChannel: Channel|undefined; let myTeams: Team[]|undefined; @@ -66,6 +68,7 @@ export const loginEntry = async ({serverUrl, user, deviceToken}: AfterLoginArgs) const [clData, prefData, teamData] = await Promise.all(promises); let chData: MyChannelsRequest|undefined; + let rData: RolesRequest|undefined; // schedule local push notification if needed if (clData.config) { @@ -119,10 +122,12 @@ export const loginEntry = async ({serverUrl, user, deviceToken}: AfterLoginArgs) } // fetch user roles - const rData = await fetchRolesIfNeeded(serverUrl, Array.from(rolesToFetch)); + rData = await fetchRolesIfNeeded(serverUrl, Array.from(rolesToFetch), true); - // select initial channel - initialChannel = selectDefaultChannelForTeam(channels!, memberships!, initialTeam!.id, rData.roles, user.locale); + // select initial channel only on Tablets + if (isTabletDevice) { + initialChannel = selectDefaultChannelForTeam(channels!, memberships!, initialTeam!.id, rData.roles, user.locale); + } } } } @@ -142,6 +147,11 @@ export const loginEntry = async ({serverUrl, user, deviceToken}: AfterLoginArgs) modelPromises.push(systemModels); } + if (initialTeam) { + const th = addTeamToTeamHistory(operator, initialTeam.id, true); + modelPromises.push(th); + } + if (initialTeam && initialChannel) { try { const tch = addChannelToTeamHistory(operator, initialTeam.id, initialChannel.id, true); @@ -151,6 +161,11 @@ export const loginEntry = async ({serverUrl, user, deviceToken}: AfterLoginArgs) } } + if (rData?.roles?.length) { + const roles = operator.handleRole({roles: rData.roles, prepareRecordsOnly: true}); + modelPromises.push(roles); + } + const models = await Promise.all(modelPromises); if (models.length) { await operator.batchRecords(models.flat() as Model[]); diff --git a/app/actions/remote/user.ts b/app/actions/remote/user.ts index 6ce09b2aeb..096a30256f 100644 --- a/app/actions/remote/user.ts +++ b/app/actions/remote/user.ts @@ -48,7 +48,12 @@ export const fetchMe = async (serverUrl: string, fetchOnly = false): Promise, Promise]>([ + client.getMe(), + client.getStatus('me'), + ]); + + user.status = userStatus.status; if (!fetchOnly) { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts index 84cebc33be..2af68827f4 100644 --- a/app/actions/websocket/index.ts +++ b/app/actions/websocket/index.ts @@ -8,7 +8,7 @@ import {fetchPostsForUnreadChannels, fetchPostsSince} from '@actions/remote/post import {fetchRoles} from '@actions/remote/role'; import {fetchConfigAndLicense} from '@actions/remote/systems'; import {fetchAllTeams, fetchTeamsChannelsAndUnreadPosts} from '@actions/remote/team'; -import {updateAllUsersSince} from '@actions/remote/user'; +import {fetchStatusByIds, updateAllUsersSince} from '@actions/remote/user'; import {General, WebsocketEvents} from '@constants'; import {SYSTEM_IDENTIFIERS} from '@constants/database'; import DatabaseManager from '@database/manager'; @@ -36,8 +36,9 @@ export async function handleFirstConnect(serverUrl: string) { if (!operator) { return; } - const config = await queryConfig(operator.database); - const lastDisconnect = await queryWebSocketLastDisconnected(operator.database); + const {database} = operator; + const config = await queryConfig(database); + const lastDisconnect = await queryWebSocketLastDisconnected(database); // ESR: 5.37 if (lastDisconnect && config.EnableReliableWebSockets !== 'true' && alreadyConnected.has(serverUrl)) { @@ -47,6 +48,7 @@ export async function handleFirstConnect(serverUrl: string) { alreadyConnected.add(serverUrl); resetWebSocketLastDisconnected(operator); + fetchStatusByIds(serverUrl, ['me']); } export function handleReconnect(serverUrl: string) { diff --git a/app/components/markdown/channel_mention/index.tsx b/app/components/markdown/channel_mention/index.tsx index 4a3406ef86..85067f7de2 100644 --- a/app/components/markdown/channel_mention/index.tsx +++ b/app/components/markdown/channel_mention/index.tsx @@ -9,8 +9,7 @@ import {useIntl} from 'react-intl'; import {StyleProp, Text, TextStyle} from 'react-native'; import {map, switchMap} from 'rxjs/operators'; -import {switchToChannel} from '@actions/local/channel'; -import {joinChannel} from '@actions/remote/channel'; +import {joinChannel, switchToChannelById} from '@actions/remote/channel'; import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database'; import {useServerUrl} from '@context/server'; import {t} from '@i18n'; @@ -96,7 +95,7 @@ const ChannelMention = ({ } if (c?.id) { - switchToChannel(serverUrl, c.id); + switchToChannelById(serverUrl, c.id); await dismissAllModals(); await popToRoot(); } diff --git a/app/components/server_icon/__snapshots__/server_icon.test.tsx.snap b/app/components/server_icon/__snapshots__/server_icon.test.tsx.snap index 8bd15dfda1..98d1ea6758 100644 --- a/app/components/server_icon/__snapshots__/server_icon.test.tsx.snap +++ b/app/components/server_icon/__snapshots__/server_icon.test.tsx.snap @@ -142,7 +142,7 @@ exports[`Server Icon Server Icon Component should match snapshot with unreads 1` "fontFamily": "OpenSans-Bold", "fontSize": 12, "height": 12, - "left": 25, + "left": 18, "lineHeight": 16.5, "minWidth": 12, "opacity": 1, @@ -150,7 +150,7 @@ exports[`Server Icon Server Icon Component should match snapshot with unreads 1` "paddingHorizontal": 0, "position": "absolute", "textAlign": "center", - "top": 5, + "top": -5, "transform": Array [ Object { "scale": 1, diff --git a/app/components/server_icon/index.tsx b/app/components/server_icon/index.tsx index 2081819091..de9f2ed9e9 100644 --- a/app/components/server_icon/index.tsx +++ b/app/components/server_icon/index.tsx @@ -29,7 +29,8 @@ const styles = StyleSheet.create({ left: 25, }, unread: { - top: 5, + left: 18, + top: -5, }, }); diff --git a/app/database/operator/server_data_operator/transformers/user.ts b/app/database/operator/server_data_operator/transformers/user.ts index 1905ea3f63..1478e54eb0 100644 --- a/app/database/operator/server_data_operator/transformers/user.ts +++ b/app/database/operator/server_data_operator/transformers/user.ts @@ -80,6 +80,9 @@ export const transformUserRecord = ({action, database, value}: TransformerArgs): user.props = raw.props || null; user.timezone = raw.timezone || null; user.isBot = raw.is_bot; + if (raw.status) { + user.status = raw.status; + } }; return prepareBaseRecord({ diff --git a/app/screens/bottom_sheet/index.tsx b/app/screens/bottom_sheet/index.tsx index ecbfd18293..fdb5b7bb93 100644 --- a/app/screens/bottom_sheet/index.tsx +++ b/app/screens/bottom_sheet/index.tsx @@ -8,7 +8,7 @@ import {Navigation as RNN} from 'react-native-navigation'; import Animated from 'react-native-reanimated'; import RNBottomSheet from 'reanimated-bottom-sheet'; -import {Navigation} from '@constants'; +import {Navigation, Screens} from '@constants'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; import {dismissModal} from '@screens/navigation'; @@ -36,7 +36,7 @@ const BottomSheet = ({closeButtonId, initialSnapIndex = 0, renderContent, snapPo if (sheetRef.current) { sheetRef.current.snapTo(lastSnap); } else { - dismissModal(); + dismissModal({componentId: Screens.BOTTOM_SHEET}); } }); @@ -48,7 +48,7 @@ const BottomSheet = ({closeButtonId, initialSnapIndex = 0, renderContent, snapPo if (sheetRef.current) { sheetRef.current.snapTo(1); } else { - dismissModal(); + dismissModal({componentId: Screens.BOTTOM_SHEET}); } return true; }); @@ -65,7 +65,7 @@ const BottomSheet = ({closeButtonId, initialSnapIndex = 0, renderContent, snapPo useEffect(() => { const navigationEvents = RNN.events().registerNavigationButtonPressedListener(({buttonId}) => { if (closeButtonId && buttonId === closeButtonId) { - dismissModal(); + dismissModal({componentId: Screens.BOTTOM_SHEET}); } }); diff --git a/app/screens/home/account/index.tsx b/app/screens/home/account/index.tsx index 8c1701d088..f265f53786 100644 --- a/app/screens/home/account/index.tsx +++ b/app/screens/home/account/index.tsx @@ -4,17 +4,15 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import withObservables from '@nozbe/with-observables'; import {useRoute} from '@react-navigation/native'; -import React, {useCallback, useEffect, useState} from 'react'; +import React, {useCallback, useState} from 'react'; import {ScrollView, View} from 'react-native'; import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated'; import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context'; import {of as of$} from 'rxjs'; import {switchMap} from 'rxjs/operators'; -import {fetchStatusInBatch} from '@actions/remote/user'; import {View as ViewConstants} from '@constants'; import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database'; -import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; import {isCustomStatusExpirySupported, isMinimumServerVersion} from '@utils/helpers'; @@ -73,13 +71,6 @@ const AccountScreen = ({currentUser, enableCustomUserStatuses, customStatusExpir const route = useRoute(); const insets = useSafeAreaInsets(); const isTablet = useIsTablet(); - const serverUrl = useServerUrl(); - - useEffect(() => { - if (currentUser) { - fetchStatusInBatch(serverUrl, currentUser.id); - } - }, []); let tabletSidebarStyle; if (isTablet) { diff --git a/app/screens/navigation.ts b/app/screens/navigation.ts index 40c38f339a..5dcc59f485 100644 --- a/app/screens/navigation.ts +++ b/app/screens/navigation.ts @@ -9,8 +9,9 @@ import {Navigation, Options, OptionsModalPresentationStyle} from 'react-native-n import tinyColor from 'tinycolor2'; import CompassIcon from '@components/compass_icon'; -import {Device, Preferences, Screens} from '@constants'; +import {Device, Screens} from '@constants'; import NavigationConstants from '@constants/navigation'; +import {getDefaultThemeByAppearance} from '@context/theme'; import EphemeralStore from '@store/ephemeral_store'; import {NavButtons} from '@typings/screens/navigation'; import {changeOpacity, setNavigatorStyles} from '@utils/theme'; @@ -19,7 +20,7 @@ import type {LaunchProps} from '@typings/launch'; const {MattermostManaged} = NativeModules; const isRunningInSplitView = MattermostManaged.isRunningInSplitView; -const appearanceControlledScreens = [Screens.SERVER, Screens.LOGIN, Screens.FORGOT_PASSWORD, Screens.MFA]; +export const appearanceControlledScreens = [Screens.SERVER, Screens.LOGIN, Screens.FORGOT_PASSWORD, Screens.MFA, Screens.SSO]; const alpha = { from: 0, @@ -30,6 +31,10 @@ const alpha = { export const loginAnimationOptions = () => { const theme = getThemeFromState(); return { + layout: { + backgroundColor: theme.centerChannelBg, + componentBackgroundColor: theme.centerChannelBg, + }, topBar: { visible: true, drawBehind: true, @@ -96,10 +101,8 @@ function getThemeFromState(): Theme { if (EphemeralStore.theme) { return EphemeralStore.theme; } - if (Appearance.getColorScheme() === 'dark') { - return Preferences.THEMES.onyx; - } - return Preferences.THEMES.denim; + + return getDefaultThemeByAppearance(); } export function resetToHome(passProps = {}) { @@ -145,7 +148,7 @@ export function resetToHome(passProps = {}) { } export function resetToSelectServer(passProps: LaunchProps) { - const theme = getThemeFromState(); + const theme = getDefaultThemeByAppearance(); const isDark = tinyColor(theme.centerChannelBg).isDark(); StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content'); @@ -565,7 +568,9 @@ export async function bottomSheet({title, renderContent, snapPoints, initialSnap snapPoints, }, { modalPresentationStyle: OptionsModalPresentationStyle.formSheet, - swipeToDismiss: true, + modal: { + swipeToDismiss: true, + }, topBar: { leftButtons: [{ id: closeButtonId, @@ -586,6 +591,6 @@ export async function bottomSheet({title, renderContent, snapPoints, initialSnap initialSnapIndex, renderContent, snapPoints, - }, {swipeToDismiss: true}); + }, {modal: {swipeToDismiss: true}}); } } diff --git a/app/utils/theme/index.ts b/app/utils/theme/index.ts index b341cb870a..6db0ba1a00 100644 --- a/app/utils/theme/index.ts +++ b/app/utils/theme/index.ts @@ -1,11 +1,12 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import merge from 'deepmerge'; import {StatusBar, StyleSheet} from 'react-native'; import tinyColor from 'tinycolor2'; import {Preferences, Screens} from '@constants'; -import {mergeNavigationOptions} from '@screens/navigation'; +import {appearanceControlledScreens, mergeNavigationOptions} from '@screens/navigation'; import EphemeralStore from '@store/ephemeral_store'; import type {Options} from 'react-native-navigation'; @@ -121,17 +122,14 @@ export function setNavigatorStyles(componentId: string, theme: Theme, additional } StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content'); - const mergeOptions = { - ...options, - ...additionalOptions, - }; + const mergeOptions = merge(options, additionalOptions); mergeNavigationOptions(componentId, mergeOptions); } export function setNavigationStackStyles(theme: Theme) { EphemeralStore.allNavigationComponentIds.forEach((componentId) => { - if (componentId !== Screens.BOTTOM_SHEET) { + if (componentId !== Screens.BOTTOM_SHEET && !appearanceControlledScreens.includes(componentId)) { setNavigatorStyles(componentId, theme); } }); diff --git a/types/api/users.d.ts b/types/api/users.d.ts index 0c45b256b6..acc28ae664 100644 --- a/types/api/users.d.ts +++ b/types/api/users.d.ts @@ -41,6 +41,7 @@ type UserProfile = { timezone?: UserTimezone; is_bot: boolean; last_picture_update: number; + status?: string; }; type UsersState = {