mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-08 04:01:21 +00:00
* Expo-Router partial migration * Full migration to ExpoRouter * i18n * add tests * Merge branch 'main' into expo-router * align package-lock.json * i18n * feedback review * feedback review * android 16kb patch * Refactored emoji picker keyboard using a state machine * set android launch_screen in drawables instead of layout * no conditional hooks * remove duplicate call in test * address feedback * fix tests and address more feedback * small corrections * small corrections * small corrections * fix draft test * fix draft test 2 * remove navigateToScreen in draff test * fix nit in draft test * remove doc file * Feedback review * fix snapshots * improve test coverage * fix i18n * Upgrade t o RN 0.83, other dependencies and new architecture (#9737) * library hardware-keyboard android new arch support fix * library rnshare android new arch support fix * library rnutils android new arch support fix * library secure-pdf-viewer add new arch support * Enable new arch on iOS and Android * update emm and network client with new-arch * update intune to use new-arch * performance improvements * fix tests * fixes post input on new-arch * fix android emoji picker * remove doc * run tests in parallel * upgrade to React Native 0.83.9, Expo 55, React 19, and New Architecture Bumps core dependencies to their latest versions: - React Native 0.77.3 → 0.83.9 (New Architecture enabled) - React 18.3.1 → 19.2.5 - Expo SDK 52 → 55 - TypeScript 5.8.3 → 6.0.3 - Node.js 22.16.0 → 24.15.0 - react-native-reanimated 3.17.3 → 4.3.0 - Various other dependency upgrades Migrates iOS AppDelegate from Objective-C to Swift, updates Android build tooling and Gradle wrapper, refreshes all patches and font assets, and adapts app code, tests, and CI workflows throughout for API changes introduced by these upgrades. * remove unecessary dismissKeyboard in notifications * feedback review * update intune to use new-arch * update @mattermost deps * missing i18n string * additional fixes * update fastlane
298 lines
12 KiB
TypeScript
298 lines
12 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {AppState, DeviceEventEmitter, Linking, Platform} from 'react-native';
|
|
import {Notifications} from 'react-native-notifications';
|
|
|
|
import {removePosts} from '@actions/local/post';
|
|
import {switchToChannelById} from '@actions/remote/channel';
|
|
import {appEntry, pushNotificationEntry} from '@actions/remote/entry';
|
|
import {fetchAndSwitchToThread} from '@actions/remote/thread';
|
|
import LocalConfig from '@assets/config.json';
|
|
import {DeepLink, Events, Launch, PushNotification} from '@constants';
|
|
import {PostTypes} from '@constants/post';
|
|
import {getDefaultThemeByAppearance} from '@context/theme';
|
|
import DatabaseManager from '@database/manager';
|
|
import {getActiveServerUrl, getServerCredentials} from '@init/credentials';
|
|
import PerformanceMetricsManager from '@managers/performance_metrics_manager';
|
|
import {getLastViewedChannelIdAndServer, getLastViewedThreadIdAndServer, getOnboardingViewed} from '@queries/app/global';
|
|
import {getAllServers} from '@queries/app/servers';
|
|
import {queryPostsByType} from '@queries/servers/post';
|
|
import {getThemeForCurrentTeam} from '@queries/servers/preference';
|
|
import {queryMyTeams} from '@queries/servers/team';
|
|
import EphemeralStore from '@store/ephemeral_store';
|
|
import {handleDeepLink, getLaunchPropsFromDeepLink} from '@utils/deep_link';
|
|
import {logInfo} from '@utils/log';
|
|
import {convertToNotificationData} from '@utils/notification';
|
|
import {removeProtocol} from '@utils/url';
|
|
|
|
import type {DeepLinkWithData, LaunchProps} from '@typings/launch';
|
|
|
|
/**
|
|
* Initial route determination for Expo Router
|
|
* Returns the route to navigate to with parameters
|
|
*/
|
|
export type ExpoRouterLaunchResult = {
|
|
route: string;
|
|
params: Record<string, any>;
|
|
};
|
|
|
|
const initialNotificationTypes = [PushNotification.NOTIFICATION_TYPE.MESSAGE, PushNotification.NOTIFICATION_TYPE.SESSION];
|
|
|
|
/**
|
|
* Determine initial route for Expo Router based on app launch conditions
|
|
*/
|
|
export async function determineInitialExpoRoute(): Promise<ExpoRouterLaunchResult> {
|
|
// Check for deep link launch
|
|
const deepLinkUrl = await Linking.getInitialURL();
|
|
if (deepLinkUrl) {
|
|
return determineRouteFromDeeplink(deepLinkUrl);
|
|
}
|
|
|
|
// Check for notification launch
|
|
const notification = await Notifications.getInitialNotification();
|
|
let tapped = Platform.select({android: true, ios: false})!;
|
|
if (Platform.OS === 'ios' && notification) {
|
|
// when a notification is received on iOS, getInitialNotification, will return the notification
|
|
// as the app will initialized cause we are using background fetch,
|
|
// that does not necessarily mean that the app was opened cause of the notification was tapped.
|
|
// Here we are going to dettermine if the notification still exists in NotificationCenter to determine if
|
|
// the app was opened because of a tap or cause of the background fetch init
|
|
const delivered = await Notifications.ios.getDeliveredNotifications();
|
|
tapped = delivered.find((d) => (d as unknown as NotificationData).ack_id === notification?.payload.ack_id) == null;
|
|
}
|
|
if (initialNotificationTypes.includes(notification?.payload?.type) && tapped) {
|
|
const notificationData = convertToNotificationData(notification!);
|
|
EphemeralStore.setProcessingNotification(notificationData.identifier);
|
|
return determineRouteFromNotification(notificationData);
|
|
}
|
|
|
|
// Normal launch
|
|
const coldStart = notification ? (tapped || AppState.currentState === 'active') : true;
|
|
return determineRoute({launchType: Launch.Normal, coldStart});
|
|
}
|
|
|
|
async function determineRouteFromDeeplink(deepLinkUrl: string): Promise<ExpoRouterLaunchResult> {
|
|
const props = getLaunchPropsFromDeepLink(deepLinkUrl, true);
|
|
return determineRoute(props);
|
|
}
|
|
|
|
async function determineRouteFromNotification(notification: NotificationWithData): Promise<ExpoRouterLaunchResult> {
|
|
const launchProps: LaunchProps = {
|
|
launchType: Launch.Notification,
|
|
coldStart: true,
|
|
};
|
|
|
|
const {payload} = notification;
|
|
launchProps.extra = notification;
|
|
let serverUrl: string | undefined;
|
|
|
|
try {
|
|
if (payload?.server_url) {
|
|
DatabaseManager.getServerDatabaseAndOperator(payload.server_url);
|
|
serverUrl = payload.server_url;
|
|
} else if (payload?.server_id) {
|
|
serverUrl = await DatabaseManager.getServerUrlFromIdentifier(payload.server_id);
|
|
}
|
|
} catch {
|
|
launchProps.launchError = true;
|
|
}
|
|
|
|
if (serverUrl) {
|
|
launchProps.serverUrl = serverUrl;
|
|
} else {
|
|
launchProps.launchError = true;
|
|
}
|
|
|
|
return determineRoute(launchProps);
|
|
}
|
|
|
|
/**
|
|
* Main launch function - matches structure of launchApp from app/init/launch.ts
|
|
* Processes launch props and routes to appropriate Expo Router destination
|
|
*
|
|
* @param props set of properties used to determine how to launch the app depending on the containing values
|
|
* @returns an Expo Router route result
|
|
*/
|
|
const determineRoute = async (props: LaunchProps): Promise<ExpoRouterLaunchResult> => {
|
|
let serverUrl: string | undefined;
|
|
switch (props?.launchType) {
|
|
case Launch.DeepLink:
|
|
if (props.extra && props.extra.type !== DeepLink.Invalid) {
|
|
const extra = props.extra as DeepLinkWithData;
|
|
const existingServer = DatabaseManager.searchUrl(extra.data!.serverUrl);
|
|
serverUrl = existingServer;
|
|
props.serverUrl = serverUrl || extra.data?.serverUrl;
|
|
if (extra.type === DeepLink.MagicLink && extra.data && 'token' in extra.data) {
|
|
const result = await handleDeepLink(extra);
|
|
if (result.error) {
|
|
props.launchError = true;
|
|
} else {
|
|
serverUrl = await getActiveServerUrl();
|
|
return determineAuthenticatedRoute({...props, serverUrl});
|
|
}
|
|
} else if (!serverUrl && extra.type !== DeepLink.Server) {
|
|
props.launchError = true;
|
|
} else if (extra.type === DeepLink.Server) {
|
|
if (removeProtocol(serverUrl) === extra.data?.serverUrl) {
|
|
props.extra = undefined;
|
|
props.launchType = Launch.Normal;
|
|
} else {
|
|
serverUrl = await getActiveServerUrl();
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case Launch.Notification: {
|
|
serverUrl = props.serverUrl;
|
|
const extra = props.extra as NotificationWithData;
|
|
const sessionExpiredNotification = Boolean(props.serverUrl && extra.payload?.type === PushNotification.NOTIFICATION_TYPE.SESSION);
|
|
if (sessionExpiredNotification) {
|
|
// Emit event - SessionManager will handle logout and relaunch
|
|
DeviceEventEmitter.emit(Events.SESSION_EXPIRED, serverUrl);
|
|
|
|
// Return current route - SessionManager will handle the rest
|
|
return determineRouteFromLaunchProps({launchType: Launch.Normal, coldStart: false});
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
serverUrl = await getActiveServerUrl();
|
|
break;
|
|
}
|
|
|
|
if (props.launchError && !serverUrl) {
|
|
serverUrl = await getActiveServerUrl();
|
|
}
|
|
|
|
cleanupEphemeralPosts();
|
|
|
|
return determineRouteFromLaunchProps({...props, serverUrl});
|
|
};
|
|
|
|
/**
|
|
* Core logic to determine route based on launch props
|
|
* This replicates the logic from app/init/launch.ts but returns Expo Router routes
|
|
*/
|
|
export async function determineRouteFromLaunchProps(props: LaunchProps): Promise<ExpoRouterLaunchResult> {
|
|
if (props.serverUrl) {
|
|
const credentials = await getServerCredentials(props.serverUrl);
|
|
if (credentials) {
|
|
try {
|
|
const {database} = DatabaseManager.getServerDatabaseAndOperator(props.serverUrl);
|
|
const theme = await getThemeForCurrentTeam(database);
|
|
EphemeralStore.setTheme(theme || getDefaultThemeByAppearance());
|
|
} catch {
|
|
EphemeralStore.setTheme(getDefaultThemeByAppearance());
|
|
}
|
|
|
|
return determineAuthenticatedRoute(props);
|
|
}
|
|
}
|
|
|
|
// No logged in user - check if should show onboarding
|
|
const onboardingViewed = LocalConfig.ShowOnboarding ? await getOnboardingViewed() : true;
|
|
const theme = getDefaultThemeByAppearance();
|
|
|
|
if (LocalConfig.ShowOnboarding && !onboardingViewed) {
|
|
return {
|
|
route: '/(unauthenticated)/onboarding',
|
|
params: {
|
|
...props,
|
|
theme: JSON.stringify(theme),
|
|
},
|
|
};
|
|
}
|
|
|
|
// Show server selection
|
|
return {
|
|
route: '/(unauthenticated)/server',
|
|
params: {
|
|
...props,
|
|
theme: JSON.stringify(theme),
|
|
},
|
|
};
|
|
}
|
|
|
|
async function determineAuthenticatedRoute(props: LaunchProps): Promise<ExpoRouterLaunchResult> {
|
|
switch (props.launchType) {
|
|
case Launch.DeepLink: {
|
|
if (props.extra?.type !== DeepLink.MagicLink) {
|
|
appEntry(props.serverUrl!);
|
|
}
|
|
break;
|
|
}
|
|
case Launch.Notification: {
|
|
const extra = props.extra as NotificationWithData;
|
|
const openPushNotification = Boolean(props.serverUrl && !props.launchError && extra.userInteraction && extra.payload?.channel_id && !extra.payload?.userInfo?.local);
|
|
if (openPushNotification) {
|
|
pushNotificationEntry(props.serverUrl!, extra.payload!, 'Notification');
|
|
break;
|
|
}
|
|
|
|
appEntry(props.serverUrl!);
|
|
break;
|
|
}
|
|
case Launch.Normal:
|
|
if (props.coldStart) {
|
|
const lastViewedChannel = await getLastViewedChannelIdAndServer();
|
|
const lastViewedThread = await getLastViewedThreadIdAndServer();
|
|
|
|
if (lastViewedThread && lastViewedThread.server_url === props.serverUrl && lastViewedThread.thread_id) {
|
|
PerformanceMetricsManager.setLoadTarget('THREAD');
|
|
fetchAndSwitchToThread(props.serverUrl!, lastViewedThread.thread_id);
|
|
} else if (lastViewedChannel && lastViewedChannel.server_url === props.serverUrl && lastViewedChannel.channel_id) {
|
|
PerformanceMetricsManager.setLoadTarget('CHANNEL');
|
|
switchToChannelById(props.serverUrl!, lastViewedChannel.channel_id);
|
|
} else {
|
|
PerformanceMetricsManager.setLoadTarget('HOME');
|
|
}
|
|
|
|
appEntry(props.serverUrl!);
|
|
}
|
|
break;
|
|
}
|
|
|
|
let nTeams = 0;
|
|
try {
|
|
const {database} = DatabaseManager.getServerDatabaseAndOperator(props.serverUrl!);
|
|
const myTeams = await queryMyTeams(database).fetch();
|
|
nTeams = myTeams.length;
|
|
} catch {
|
|
nTeams = 0;
|
|
}
|
|
|
|
if (nTeams === 0) {
|
|
logInfo('Route app to Teams screen');
|
|
return {
|
|
route: '/(authenticated)/select_team',
|
|
params: props,
|
|
};
|
|
}
|
|
|
|
logInfo('Route app to Home screen');
|
|
return {
|
|
route: '/(authenticated)/(home)',
|
|
params: props,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Cleanup ephemeral posts from all server databases
|
|
* Ephemeral posts are temporary posts (like command responses) that should not persist
|
|
*/
|
|
async function cleanupEphemeralPosts() {
|
|
const servers = await getAllServers();
|
|
|
|
await Promise.all(
|
|
servers.map(async (server) => {
|
|
const database = DatabaseManager.serverDatabases[server.url]?.database;
|
|
if (!database) {
|
|
return Promise.resolve();
|
|
}
|
|
const posts = await queryPostsByType(database, PostTypes.EPHEMERAL).fetch();
|
|
return removePosts(server.url, posts);
|
|
}),
|
|
);
|
|
}
|