diff --git a/app/actions/local/channel.ts b/app/actions/local/channel.ts index da5491630e..090441b510 100644 --- a/app/actions/local/channel.ts +++ b/app/actions/local/channel.ts @@ -288,17 +288,17 @@ export const updateLastPostAt = async (serverUrl: string, channelId: string, las member.prepareUpdate((m) => { m.lastPostAt = lastPostAt; }); - } - try { - if (!prepareRecordsOnly) { - await operator.batchRecords([member]); + try { + if (!prepareRecordsOnly) { + await operator.batchRecords([member]); + } + } catch (error) { + return {error}; } - } catch (error) { - return {error}; } - return {member}; + return {member: undefined}; }; export async function updateChannelsDisplayName(serverUrl: string, channels: ChannelModel[], users: UserProfile[], prepareRecordsOnly = false) { diff --git a/app/actions/remote/channel.ts b/app/actions/remote/channel.ts index 806a4cf455..57b857531f 100644 --- a/app/actions/remote/channel.ts +++ b/app/actions/remote/channel.ts @@ -259,7 +259,7 @@ export const fetchMyChannel = async (serverUrl: string, teamId: string, channelI }; export const fetchMissingSidebarInfo = async (serverUrl: string, directChannels: Channel[], locale?: string, teammateDisplayNameSetting?: string, exludeUserId?: string, fetchOnly = false) => { - const channelIds = directChannels.map((dc) => dc.id); + const channelIds = directChannels.sort((a, b) => b.last_post_at - a.last_post_at).map((dc) => dc.id); const result = await fetchProfilesPerChannels(serverUrl, channelIds, exludeUserId, false); if (result.error) { return {error: result.error}; diff --git a/app/actions/remote/entry/app.ts b/app/actions/remote/entry/app.ts index 2f7e62ab43..c829d866e7 100644 --- a/app/actions/remote/entry/app.ts +++ b/app/actions/remote/entry/app.ts @@ -13,9 +13,9 @@ import {queryCurrentUser} from '@queries/servers/user'; import {deleteV1Data} from '@utils/file'; import {isTablet} from '@utils/helpers'; -import {AppEntryData, AppEntryError, deferredAppEntryActions, fetchAppEntryData} from './common'; +import {AppEntryData, AppEntryError, deferredAppEntryActions, fetchAppEntryData, syncOtherServers} from './common'; -export const appEntry = async (serverUrl: string) => { +export const appEntry = async (serverUrl: string, since = 0) => { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (!operator) { return {error: `${serverUrl} database not found`}; @@ -24,7 +24,7 @@ export const appEntry = async (serverUrl: string) => { const tabletDevice = await isTablet(); const currentTeamId = await queryCurrentTeamId(database); - const lastDisconnectedAt = await queryWebSocketLastDisconnected(database); + const lastDisconnectedAt = (await queryWebSocketLastDisconnected(database)) || since; const fetchedData = await fetchAppEntryData(serverUrl, lastDisconnectedAt, currentTeamId); const fetchedError = (fetchedData as AppEntryError).error; @@ -87,6 +87,11 @@ export const appEntry = async (serverUrl: string) => { const {config, license} = await queryCommonSystemValues(database); deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId); + if (!since) { + // Load data from other servers + syncOtherServers(serverUrl); + } + const error = teamData.error || chData?.error || prefData.error || meData.error; return {error, userId: meData?.user?.id}; }; diff --git a/app/actions/remote/entry/common.ts b/app/actions/remote/entry/common.ts index 0b2a788908..8cf9ec22dc 100644 --- a/app/actions/remote/entry/common.ts +++ b/app/actions/remote/entry/common.ts @@ -13,6 +13,8 @@ import DatabaseManager from '@database/manager'; import {getPreferenceValue, getTeammateNameDisplaySetting} from '@helpers/api/preference'; import {selectDefaultTeam} from '@helpers/api/team'; import {DEFAULT_LOCALE} from '@i18n'; +import NetworkManager from '@init/network_manager'; +import {queryAllServers} from '@queries/app/servers'; import {queryAllChannelsForTeam} from '@queries/servers/channel'; import {queryConfig} from '@queries/servers/system'; import {queryAvailableTeamIds, queryMyTeams} from '@queries/servers/team'; @@ -193,3 +195,40 @@ export const deferredAppEntryActions = async ( updateAllUsersSince(serverUrl, since); }; + +export const syncOtherServers = async (serverUrl: string) => { + const database = DatabaseManager.appDatabase?.database; + if (database) { + const servers = await queryAllServers(database); + for (const server of servers) { + if (server.url !== serverUrl && server.lastActiveAt > 0) { + syncAllChannelMembers(server.url); + } + } + } +}; + +const syncAllChannelMembers = async (serverUrl: string) => { + const database = DatabaseManager.serverDatabases[serverUrl]?.database; + if (!database) { + return; + } + + let client; + try { + client = NetworkManager.getClient(serverUrl); + } catch { + return; + } + + try { + const myTeams = await client.getMyTeams(); + let excludeDirect = false; + for (const myTeam of myTeams) { + fetchMyChannelsForTeam(serverUrl, myTeam.id, false, 0, false, excludeDirect); + excludeDirect = true; + } + } catch { + // Do nothing + } +}; diff --git a/app/actions/remote/entry/notification.ts b/app/actions/remote/entry/notification.ts index fe94f8092f..19f8c0f742 100644 --- a/app/actions/remote/entry/notification.ts +++ b/app/actions/remote/entry/notification.ts @@ -17,7 +17,7 @@ import EphemeralStore from '@store/ephemeral_store'; import {isTablet} from '@utils/helpers'; import {emitNotificationError} from '@utils/notification'; -import {AppEntryData, AppEntryError, deferredAppEntryActions, fetchAppEntryData} from './common'; +import {AppEntryData, AppEntryError, deferredAppEntryActions, fetchAppEntryData, syncOtherServers} from './common'; export const pushNotificationEntry = async (serverUrl: string, notification: NotificationWithData) => { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; @@ -143,6 +143,7 @@ export const pushNotificationEntry = async (serverUrl: string, notification: Not const {config, license} = await queryCommonSystemValues(operator.database); deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, selectedChannelId); + syncOtherServers(serverUrl); const error = teamData.error || chData?.error || prefData.error || meData.error; return {error, userId: meData?.user?.id}; }; diff --git a/app/actions/remote/notifications.ts b/app/actions/remote/notifications.ts index d794ecf502..10c1c380f6 100644 --- a/app/actions/remote/notifications.ts +++ b/app/actions/remote/notifications.ts @@ -27,7 +27,12 @@ const fetchNotificationData = async (serverUrl: string, notification: Notificati } try { - const channelId = notification.payload!.channel_id!; + const channelId = notification.payload?.channel_id; + + if (!channelId) { + return {error: 'No chanel Id was specified'}; + } + const {database} = operator; const system = await queryCommonSystemValues(database); let teamId = notification.payload?.team_id; diff --git a/app/actions/remote/session.ts b/app/actions/remote/session.ts index 4e6afe3eec..afb5febd68 100644 --- a/app/actions/remote/session.ts +++ b/app/actions/remote/session.ts @@ -5,6 +5,7 @@ import {DeviceEventEmitter} from 'react-native'; import {autoUpdateTimezone, getDeviceTimezone, isTimezoneEnabled} from '@actions/local/timezone'; import {Database, Events} from '@constants'; +import {SYSTEM_IDENTIFIERS} from '@constants/database'; import DatabaseManager from '@database/manager'; import {getServerCredentials} from '@init/credentials'; import NetworkManager from '@init/network_manager'; @@ -23,11 +24,12 @@ import type {LoginArgs} from '@typings/database/database'; const HTTP_UNAUTHORIZED = 401; export const completeLogin = async (serverUrl: string, user: UserProfile) => { - 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 {config, license}: { config: Partial; license: Partial } = await queryCommonSystemValues(database); if (!Object.keys(config)?.length || !Object.keys(license)?.length) { @@ -45,10 +47,17 @@ export const completeLogin = async (serverUrl: string, user: UserProfile) => { fetchDataRetentionPolicy(serverUrl); } + await DatabaseManager.setActiveServerDatabase(serverUrl); + // Start websocket const credentials = await getServerCredentials(serverUrl); if (credentials?.token) { WebsocketManager.createClient(serverUrl, credentials.token); + return operator.handleSystem({systems: [{ + id: SYSTEM_IDENTIFIERS.WEBSOCKET, + value: 0, + }], + prepareRecordsOnly: false}); } return null; }; @@ -120,7 +129,7 @@ export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaTo displayName: serverDisplayName, }, }); - await DatabaseManager.setActiveServerDatabase(serverUrl); + await server?.operator.handleUsers({users: [user], prepareRecordsOnly: false}); await server?.operator.handleSystem({ systems: [{ @@ -208,7 +217,6 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser displayName: serverDisplayName, }, }); - await DatabaseManager.setActiveServerDatabase(serverUrl); deviceToken = await queryDeviceToken(database); user = await client.getMe(); await server?.operator.handleUsers({users: [user], prepareRecordsOnly: false}); diff --git a/app/actions/remote/user.ts b/app/actions/remote/user.ts index 096a30256f..077f31b20c 100644 --- a/app/actions/remote/user.ts +++ b/app/actions/remote/user.ts @@ -2,6 +2,7 @@ // See LICENSE.txt for license information. import {Model, Q} from '@nozbe/watermelondb'; +import {chunk} from 'lodash'; import {updateChannelsDisplayName} from '@actions/local/channel'; import {updateRecentCustomStatuses, updateLocalUser} from '@actions/local/user'; @@ -114,8 +115,14 @@ export const fetchProfilesInChannel = async (serverUrl: string, channelId: strin export const fetchProfilesPerChannels = async (serverUrl: string, channelIds: string[], excludeUserId?: string, fetchOnly = false): Promise => { try { - const requests = channelIds.map((id) => fetchProfilesInChannel(serverUrl, id, excludeUserId, true)); - const data = await Promise.all(requests); + // Batch fetching profiles per channel by chunks of 50 + const channels = chunk(channelIds, 50); + const data: ProfilesInChannelRequest[] = []; + for await (const cIds of channels) { + const requests = cIds.map((id) => fetchProfilesInChannel(serverUrl, id, excludeUserId, true)); + const response = await Promise.all(requests); + data.push(...response); + } if (!fetchOnly) { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; diff --git a/app/actions/websocket/channel.ts b/app/actions/websocket/channel.ts index a6f061bdba..f74ae749f2 100644 --- a/app/actions/websocket/channel.ts +++ b/app/actions/websocket/channel.ts @@ -25,7 +25,6 @@ export async function handleUserAddedToChannelEvent(serverUrl: string, msg: any) } const currentUser = await queryCurrentUser(database.database); const {team_id: teamId, channel_id: channelId, user_id: userId} = msg.data; - const models: Model[] = []; try { diff --git a/app/actions/websocket/posts.ts b/app/actions/websocket/posts.ts index 0e37cc1540..84cc9a34ac 100644 --- a/app/actions/websocket/posts.ts +++ b/app/actions/websocket/posts.ts @@ -61,7 +61,10 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag // Ensure the channel membership let myChannel = await queryMyChannel(operator.database, post.channel_id); if (myChannel) { - await updateLastPostAt(serverUrl, post.channel_id, post.create_at, false); + const {member} = await updateLastPostAt(serverUrl, post.channel_id, post.create_at, false); + if (member) { + myChannel = member; + } } else { const myChannelRequest = await fetchMyChannel(serverUrl, '', post.channel_id, true); if (myChannelRequest.error) { @@ -142,7 +145,7 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag models.push(viewedAt); } } else { - const hasMentions = msg.data.mentions.includes(currentUserId); + const hasMentions = msg.data.mentions?.includes(currentUserId); preparedMyChannelHack(myChannel); const {member: unreadAt} = await markChannelAsUnread( serverUrl, diff --git a/app/client/rest/base.ts b/app/client/rest/base.ts index 5cbe56029e..9aaa42079f 100644 --- a/app/client/rest/base.ts +++ b/app/client/rest/base.ts @@ -7,7 +7,7 @@ import {Events} from '@constants'; import {t} from '@i18n'; import {Analytics, create} from '@init/analytics'; import {setServerCredentials} from '@init/credentials'; -import {semverFromServerVersion} from '@utils/supported_server'; +import {semverFromServerVersion} from '@utils/server'; import * as ClientConstants from './constants'; import ClientError from './error'; diff --git a/app/components/loading/index.tsx b/app/components/loading/index.tsx index 3ef7333cd8..075140e465 100644 --- a/app/components/loading/index.tsx +++ b/app/components/loading/index.tsx @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import LottieView from 'lottie-react-native'; -import React from 'react'; +import React, {useEffect, useRef} from 'react'; import {StyleSheet, View, ViewStyle} from 'react-native'; type LoadingProps = { @@ -12,14 +12,28 @@ type LoadingProps = { } const Loading = ({containerStyle, style, color}: LoadingProps) => { + const lottieRef = useRef(null); + + // This is a workaround as it seems that autoPlay does not work properly on + // newer versions of RN + useEffect(() => { + const animationFrame = requestAnimationFrame(() => { + lottieRef.current?.reset(); + lottieRef.current?.play(); + }); + + return () => cancelAnimationFrame(animationFrame); + }, []); + return ( ); 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 98d1ea6758..6fd6cf2db6 100644 --- a/app/components/server_icon/__snapshots__/server_icon.test.tsx.snap +++ b/app/components/server_icon/__snapshots__/server_icon.test.tsx.snap @@ -76,7 +76,7 @@ exports[`Server Icon Server Icon Component should match snapshot with mentions 1 "fontFamily": "OpenSans-Bold", "fontSize": 12, "height": 22, - "left": 25, + "left": 13, "lineHeight": 16.5, "minWidth": 26, "opacity": 1, @@ -84,7 +84,7 @@ exports[`Server Icon Server Icon Component should match snapshot with mentions 1 "paddingHorizontal": 5, "position": "absolute", "textAlign": "center", - "top": -1, + "top": -8, "transform": Array [ Object { "scale": 1, diff --git a/app/components/server_icon/index.tsx b/app/components/server_icon/index.tsx index de9f2ed9e9..28207a175d 100644 --- a/app/components/server_icon/index.tsx +++ b/app/components/server_icon/index.tsx @@ -26,7 +26,8 @@ type Props = { const styles = StyleSheet.create({ badge: { - left: 25, + left: 13, + top: -8, }, unread: { left: 18, diff --git a/app/components/server_version/index.tsx b/app/components/server_version/index.tsx index 2608540bdb..3a21c2fd13 100644 --- a/app/components/server_version/index.tsx +++ b/app/components/server_version/index.tsx @@ -11,7 +11,7 @@ import {map, switchMap} from 'rxjs/operators'; import {SupportedServer} from '@constants'; import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database'; import {isMinimumServerVersion} from '@utils/helpers'; -import {unsupportedServer} from '@utils/supported_server'; +import {unsupportedServer} from '@utils/server'; import {isSystemAdmin} from '@utils/user'; import type {WithDatabaseArgs} from '@typings/database/database'; diff --git a/app/database/components/index.tsx b/app/database/components/index.tsx index 7590e08413..2e65c39b6b 100644 --- a/app/database/components/index.tsx +++ b/app/database/components/index.tsx @@ -29,14 +29,16 @@ export function withServerDatabase(Component: ComponentType): ComponentTyp ) : undefined; if (server) { - const serverDatabase = - DatabaseManager.serverDatabases[server?.url]?.database; + const database = + DatabaseManager.serverDatabases[server.url]?.database; - setState({ - database: serverDatabase, - serverUrl: server?.url, - serverDisplayName: server?.displayName, - }); + if (database) { + setState({ + database, + serverUrl: server.url, + serverDisplayName: server.displayName, + }); + } } else { setState(undefined); } @@ -55,7 +57,10 @@ export function withServerDatabase(Component: ComponentType): ComponentTyp } return ( - + diff --git a/app/database/manager/index.ts b/app/database/manager/index.ts index 6a62db8baa..e2c1ebb04b 100644 --- a/app/database/manager/index.ts +++ b/app/database/manager/index.ts @@ -376,9 +376,9 @@ class DatabaseManager { const databaseShm = `${androidFilesDir}${databaseName}.db-shm`; const databaseWal = `${androidFilesDir}${databaseName}.db-wal`; - FileSystem.deleteAsync(databaseFile); - FileSystem.deleteAsync(databaseShm); - FileSystem.deleteAsync(databaseWal); + FileSystem.deleteAsync(databaseFile, {idempotent: true}); + FileSystem.deleteAsync(databaseShm, {idempotent: true}); + FileSystem.deleteAsync(databaseWal, {idempotent: true}); }; /** diff --git a/app/init/global_event_handler.ts b/app/init/global_event_handler.ts index 44ab580d99..5c32e20a77 100644 --- a/app/init/global_event_handler.ts +++ b/app/init/global_event_handler.ts @@ -90,6 +90,8 @@ class GlobalEventHandler { NetworkManager.invalidateClient(serverUrl); WebsocketManager.invalidateClient(serverUrl); + + const activeServerUrl = await DatabaseManager.getActiveServerUrl(); await DatabaseManager.deleteServerDatabase(serverUrl); const analyticsClient = analytics.get(serverUrl); @@ -102,11 +104,15 @@ class GlobalEventHandler { this.clearCookiesForServer(serverUrl); deleteFileCache(serverUrl); - if (!Object.keys(DatabaseManager.serverDatabases).length) { - EphemeralStore.theme = undefined; - } + if (activeServerUrl === serverUrl) { + let launchType: LaunchType = LaunchType.AddServer; + if (!Object.keys(DatabaseManager.serverDatabases).length) { + EphemeralStore.theme = undefined; + launchType = LaunchType.Normal; + } - relaunchApp({launchType: LaunchType.Normal}, true); + relaunchApp({launchType}, true); + } }; onServerConfigChanged = ({serverUrl, config}: {serverUrl: string; config: ClientConfig}) => { diff --git a/app/init/launch.ts b/app/init/launch.ts index 1a57f1391a..95b438ddf2 100644 --- a/app/init/launch.ts +++ b/app/init/launch.ts @@ -142,14 +142,9 @@ const launchToHome = async (props: LaunchProps) => { break; } - const passProps = { - skipMetrics: true, - ...props, - }; - // eslint-disable-next-line no-console console.log('Launch app in Home screen'); - resetToHome(passProps); + resetToHome(props); }; const launchToServer = (props: LaunchProps, resetNavigation: Boolean) => { diff --git a/app/screens/home/account/components/options/logout/index.tsx b/app/screens/home/account/components/options/logout/index.tsx index 0adbe7a5df..aa9d4f81e5 100644 --- a/app/screens/home/account/components/options/logout/index.tsx +++ b/app/screens/home/account/components/options/logout/index.tsx @@ -1,15 +1,15 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useCallback, useEffect, useState} from 'react'; +import React, {useCallback} from 'react'; +import {useIntl} from 'react-intl'; import {TextStyle, View} from 'react-native'; import {logout} from '@actions/remote/session'; import DrawerItem from '@components/drawer_item'; import FormattedText from '@components/formatted_text'; -import {useServerUrl} from '@context/server'; -import DatabaseManager from '@database/manager'; -import {queryServer} from '@queries/app/servers'; +import {useServerDisplayName, useServerUrl} from '@context/server'; +import {alertServerLogout} from '@utils/server'; import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; @@ -34,22 +34,19 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ const Settings = ({style, theme}: Props) => { const styles = getStyleSheet(theme); + const intl = useIntl(); const serverUrl = useServerUrl(); - const [serverName, setServerName] = useState(serverUrl); - const onLogout = useCallback(preventDoubleTap(() => { - logout(serverUrl); - }), []); + const serverDisplayName = useServerDisplayName(); - useEffect(() => { - const appDatabase = DatabaseManager.appDatabase?.database; - if (appDatabase) { - queryServer(appDatabase, serverUrl).then((server) => { - if (server) { - setServerName(server.displayName); - } - }); - } - }, [serverUrl]); + const onLogout = useCallback(preventDoubleTap(() => { + alertServerLogout( + serverDisplayName, + () => { + logout(serverUrl); + }, + intl, + ); + }), [serverDisplayName, serverUrl, intl]); return ( { diff --git a/app/screens/home/channel_list/servers/index.tsx b/app/screens/home/channel_list/servers/index.tsx index 722cb623f0..ea418d1ace 100644 --- a/app/screens/home/channel_list/servers/index.tsx +++ b/app/screens/home/channel_list/servers/index.tsx @@ -92,6 +92,7 @@ export default function Servers() { for (const [key, map] of subscriptionsToRemove) { map.subscription?.unsubscribe(); subscriptions.delete(key); + updateTotal(); } for (const server of servers) { @@ -103,9 +104,10 @@ export default function Servers() { }; subscriptions.set(url, unreads); unreads.subscription = subscribeUnreadAndMentionsByServer(url, unreadsSubscription); - } else if (subscriptions.has(url)) { + } else if ((!lastActiveAt || url === currentServerUrl) && subscriptions.has(url)) { subscriptions.get(url)?.subscription?.unsubscribe(); subscriptions.delete(url); + updateTotal(); } } }; @@ -129,7 +131,7 @@ export default function Servers() { renderContent, snapPoints, theme, - title: intl.formatMessage({id: 'servers.create_button', defaultMessage: 'Add a server'}), + title: intl.formatMessage({id: 'your.servers', defaultMessage: 'Your servers'}), }); } }, [isTablet, theme]); diff --git a/app/screens/home/channel_list/servers/servers_list/index.tsx b/app/screens/home/channel_list/servers/servers_list/index.tsx index c726b5cc4b..76730ab77a 100644 --- a/app/screens/home/channel_list/servers/servers_list/index.tsx +++ b/app/screens/home/channel_list/servers/servers_list/index.tsx @@ -7,8 +7,10 @@ import {ListRenderItemInfo, StyleSheet, View} from 'react-native'; import {FlatList} from 'react-native-gesture-handler'; import {useServerUrl} from '@context/server'; +import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; import BottomSheetContent from '@screens/bottom_sheet/content'; +import {addNewServer} from '@utils/server'; import ServerItem from './server_item'; @@ -33,10 +35,10 @@ const ServerList = ({servers}: Props) => { const intl = useIntl(); const isTablet = useIsTablet(); const serverUrl = useServerUrl(); + const theme = useTheme(); - const onAddServer = useCallback(() => { - // eslint-disable-next-line no-console - console.log('Lets add a server'); + const onAddServer = useCallback(async () => { + addNewServer(theme); }, [servers]); const renderServer = useCallback(({item: t}: ListRenderItemInfo) => { diff --git a/app/screens/home/channel_list/servers/servers_list/server_item/index.tsx b/app/screens/home/channel_list/servers/servers_list/server_item/index.tsx index 2b2257b146..e974f197fa 100644 --- a/app/screens/home/channel_list/servers/servers_list/server_item/index.tsx +++ b/app/screens/home/channel_list/servers/servers_list/server_item/index.tsx @@ -7,17 +7,23 @@ import {DeviceEventEmitter, Text, View} from 'react-native'; import {RectButton} from 'react-native-gesture-handler'; import Swipeable from 'react-native-gesture-handler/Swipeable'; +import {appEntry} from '@actions/remote/entry'; +import {logout} from '@actions/remote/session'; import CompassIcon from '@components/compass_icon'; +import Loading from '@components/loading'; import ServerIcon from '@components/server_icon'; import {Events} from '@constants'; import {useTheme} from '@context/theme'; +import DatabaseManager from '@database/manager'; import {subscribeServerUnreadAndMentions} from '@database/subscription/unreads'; -import WebsocketManager from '@init/websocket_manager'; +import {dismissBottomSheet} from '@screens/navigation'; +import {addNewServer, alertServerLogout, alertServerRemove} from '@utils/server'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; import {removeProtocol, stripTrailingSlashes} from '@utils/url'; import Options from './options'; +import WebSocket from './websocket'; import type ServersModel from '@typings/database/models/app/servers'; import type MyChannelModel from '@typings/database/models/servers/my_channel'; @@ -63,15 +69,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ logout: { backgroundColor: theme.centerChannelBg, borderRadius: 8, - height: 16, - left: 40, + height: 18, + left: 42, position: 'absolute', top: 11, - width: 16, - }, - nameContainer: { - alignItems: 'center', - flexDirection: 'row', + width: 18, }, name: { color: theme.centerChannelColor, @@ -80,7 +82,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ offline: { opacity: 0.5, }, - row: {flexDirection: 'row'}, + row: {flexDirection: 'row', alignItems: 'center'}, + serverIcon: { + borderColor: 'transparent', + borderWidth: 1, + height: 72, + justifyContent: 'center', + width: 45, + }, unread: { top: -2, left: 25, @@ -89,21 +98,24 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ color: changeOpacity(theme.centerChannelColor, 0.72), ...typography('Body', 75, 'Regular'), }, - websocket: { - marginLeft: 7, - marginTop: 4, + switching: { + height: 40, + width: 40, + justifyContent: 'center', }, })); const ServerItem = ({isActive, server}: Props) => { const intl = useIntl(); const theme = useTheme(); + const [switching, setSwitching] = useState(false); const [badge, setBadge] = useState({isUnread: false, mentions: 0}); const styles = getStyleSheet(theme); const swipeable = useRef(); const subscription = useRef(); - const websocketError = server.lastActiveAt ? !WebsocketManager.isConnected(server.url) : false; + const database = DatabaseManager.serverDatabases[server.url]?.database; let displayName = server.displayName; + if (server.url === server.displayName) { displayName = intl.formatMessage({id: 'servers.default', defaultMessage: 'Default Server'}); } @@ -119,6 +131,30 @@ const ServerItem = ({isActive, server}: Props) => { setBadge({isUnread, mentions}); }; + const logoutServer = async () => { + await logout(server.url); + + if (isActive) { + dismissBottomSheet(); + } else { + DeviceEventEmitter.emit(Events.SWIPEABLE, ''); + } + }; + + const removeServer = async () => { + if (server.lastActiveAt > 0) { + await logout(server.url); + } + + if (isActive) { + dismissBottomSheet(); + } else { + DeviceEventEmitter.emit(Events.SWIPEABLE, ''); + } + + await DatabaseManager.destroyServerDatabase(server.url); + }; + const containerStyle = useMemo(() => { const style = [styles.container]; if (isActive) { @@ -137,23 +173,39 @@ const ServerItem = ({isActive, server}: Props) => { return style; }, [server.lastActiveAt]); - const onServerPressed = useCallback(() => { + const handleLogin = useCallback(() => { + addNewServer(theme, server.url, displayName); + }, [server, theme, intl]); + + const handleEdit = useCallback(() => { + // eslint-disable-next-line no-console + console.log('ON EDIT'); + }, [server]); + + const handleLogout = useCallback(async () => { + alertServerLogout(server.displayName, logoutServer, intl); + }, [isActive, intl, server]); + + const handleRemove = useCallback(() => { + alertServerRemove(server.displayName, removeServer, intl); + }, [isActive, server, intl]); + + const onServerPressed = useCallback(async () => { if (isActive) { - // eslint-disable-next-line no-console - console.log('ACTIVE SERVER', server.displayName); DeviceEventEmitter.emit(Events.CLOSE_BOTTOM_SHEET); return; } if (server.lastActiveAt) { - // eslint-disable-next-line no-console - console.log('SWITCH TO SERVER', server.displayName); + setSwitching(true); + await appEntry(server.url, Date.now()); + await dismissBottomSheet(); + DatabaseManager.setActiveServerDatabase(server.url); return; } - // eslint-disable-next-line no-console - console.log('LOGIN TO SERVER', server.displayName); - }, [server]); + handleLogin(); + }, [server, isActive, theme, intl]); const onSwipeableWillOpen = useCallback(() => { DeviceEventEmitter.emit(Events.SWIPEABLE, server.url); @@ -162,11 +214,15 @@ const ServerItem = ({isActive, server}: Props) => { const renderActions = useCallback((progress) => { return ( ); - }, [server]); + }, [isActive, server, theme, intl]); useEffect(() => { const listener = DeviceEventEmitter.addListener(Events.SWIPEABLE, (url: string) => { @@ -182,9 +238,10 @@ const ServerItem = ({isActive, server}: Props) => { if (!isActive) { if (server.lastActiveAt && !subscription.current) { subscription.current = subscribeServerUnreadAndMentions(server.url, unreadsSubscription); - } else if (!server.lastActiveAt && subscription.current) { - subscription.current.unsubscribe(); + } else if (!server.lastActiveAt) { + subscription.current?.unsubscribe(); subscription.current = undefined; + setBadge({isUnread: false, mentions: 0}); } } @@ -195,62 +252,68 @@ const ServerItem = ({isActive, server}: Props) => { }, [server.lastActiveAt, isActive]); return ( - + - - - {!server.lastActiveAt && - - - - } - - - - + + + {!switching && + + } + {switching && + + } + {displayName} - {websocketError && - - } + {removeProtocol(stripTrailingSlashes(server.url))} - {removeProtocol(stripTrailingSlashes(server.url))} - - - - + {!server.lastActiveAt && !switching && + + + + } + + + + {Boolean(database) && server.lastActiveAt > 0 && + + } + ); }; diff --git a/app/screens/home/channel_list/servers/servers_list/server_item/options/index.tsx b/app/screens/home/channel_list/servers/servers_list/server_item/options/index.tsx index c27bfcf530..9f3eb7fe1b 100644 --- a/app/screens/home/channel_list/servers/servers_list/server_item/options/index.tsx +++ b/app/screens/home/channel_list/servers/servers_list/server_item/options/index.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useCallback} from 'react'; +import React from 'react'; import {useIntl} from 'react-intl'; import {Animated, StyleSheet, View} from 'react-native'; @@ -13,6 +13,10 @@ import Option, {OPTION_SIZE} from './option'; import type ServersModel from '@typings/database/models/app/servers'; type Props = { + onEdit: () => void; + onLogin: () => void; + onLogout: () => void; + onRemove: () => void; progress: Animated.AnimatedInterpolation; server: ServersModel; } @@ -27,7 +31,7 @@ const styles = StyleSheet.create({ right: {borderTopRightRadius: 8, borderBottomRightRadius: 8}, }); -const ServerOptions = ({progress, server}: Props) => { +const ServerOptions = ({onEdit, onLogin, onLogout, onRemove, progress, server}: Props) => { const intl = useIntl(); const theme = useTheme(); const isLoggedIn = server.lastActiveAt > 0; @@ -41,26 +45,6 @@ const ServerOptions = ({progress, server}: Props) => { defaultMessage: 'Log in', }); - const onEdit = useCallback(() => { - // eslint-disable-next-line no-console - console.log('ON EDIT'); - }, [server]); - - const onLogin = useCallback(() => { - // eslint-disable-next-line no-console - console.log('ON Login'); - }, [server]); - - const onLogout = useCallback(() => { - // eslint-disable-next-line no-console - console.log('ON Logout'); - }, [server]); - - const onRemove = useCallback(() => { - // eslint-disable-next-line no-console - console.log('ON Remove'); - }, [server]); - return (