mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-06 10:50:13 +00:00
- switchToThread: wait for CHANNEL_LIST to be top of stack instead of the unreachable HOME tab-group segment, fixing the ~30s delay when opening thread notifications while the app is running - reduce wait fallback timeouts to 1s - icon.tsx: re-subscribe to the sender's avatar when senderId changes, fixing stale avatars shown on bursty/rapid notifications
244 lines
7.9 KiB
TypeScript
244 lines
7.9 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {useEffect, useState} from 'react';
|
|
import {BehaviorSubject} from 'rxjs';
|
|
|
|
import type {NavigationState as ExpoNavigationState, NavigationRoute, ParamListBase} from '@react-navigation/native';
|
|
import type {AvailableScreens} from '@typings/screens/navigation';
|
|
|
|
interface NavigationState {
|
|
screenStack: AvailableScreens[];
|
|
}
|
|
|
|
const initialState: NavigationState = {
|
|
screenStack: [],
|
|
};
|
|
|
|
class NavigationStoreSingleton {
|
|
private stateSubject = new BehaviorSubject<NavigationState>(initialState);
|
|
private screenSubject = new BehaviorSubject<AvailableScreens | undefined>(undefined);
|
|
private tosOpen = false;
|
|
|
|
// Observable access
|
|
state$ = this.stateSubject.asObservable();
|
|
currentScreen$ = this.screenSubject.asObservable();
|
|
|
|
// Getters
|
|
get state() {
|
|
return this.stateSubject.value;
|
|
}
|
|
|
|
getVisibleScreen() {
|
|
return this.state.screenStack[this.state.screenStack.length - 1];
|
|
}
|
|
|
|
getScreensInStack() {
|
|
return this.state.screenStack;
|
|
}
|
|
|
|
isScreenInStack(screenId: AvailableScreens) {
|
|
return this.state.screenStack.includes(screenId);
|
|
}
|
|
|
|
isModalOpen(): boolean {
|
|
return this.state.screenStack.includes('(modals)');
|
|
}
|
|
|
|
// State management
|
|
reset() {
|
|
this.stateSubject.next(initialState);
|
|
this.screenSubject.next(undefined);
|
|
this.tosOpen = false;
|
|
}
|
|
|
|
updateFromNavigationState(navState: ExpoNavigationState | undefined) {
|
|
if (!navState) {
|
|
return;
|
|
}
|
|
|
|
// Update screenStack with current screens
|
|
const screenStack: AvailableScreens[] = [];
|
|
this.extractScreenIds(navState, screenStack);
|
|
|
|
// Update the state if the screen stack has changed
|
|
const stackChanged = screenStack.length !== this.state.screenStack.length ||
|
|
screenStack.some((screen, index) => screen !== this.state.screenStack[index]);
|
|
|
|
if (stackChanged) {
|
|
// Get the visible screen before updating state
|
|
const visibleScreen = screenStack[screenStack.length - 1];
|
|
const prevVisibleScreen = this.state.screenStack[this.state.screenStack.length - 1];
|
|
|
|
// Update the state
|
|
this.stateSubject.next({...this.state, screenStack});
|
|
|
|
// Update the current screen if it changed
|
|
if (visibleScreen !== prevVisibleScreen) {
|
|
this.screenSubject.next(visibleScreen);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Async wait utilities
|
|
waitUntilScreenHasLoaded(screenId: AvailableScreens): Promise<void> {
|
|
return new Promise<void>((resolve) => {
|
|
if (this.isScreenInStack(screenId)) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
const subscription = this.state$.subscribe((state) => {
|
|
if (state.screenStack.includes(screenId)) {
|
|
subscription.unsubscribe();
|
|
resolve();
|
|
}
|
|
});
|
|
|
|
setTimeout(() => {
|
|
subscription.unsubscribe();
|
|
resolve();
|
|
}, 1000);
|
|
});
|
|
}
|
|
|
|
waitUntilScreenIsTop(screenId: AvailableScreens): Promise<void> {
|
|
return new Promise<void>((resolve) => {
|
|
if (this.getVisibleScreen() === screenId) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
const subscription = this.state$.subscribe((state) => {
|
|
const topScreen = state.screenStack[state.screenStack.length - 1];
|
|
if (topScreen === screenId) {
|
|
subscription.unsubscribe();
|
|
resolve();
|
|
}
|
|
});
|
|
|
|
setTimeout(() => {
|
|
subscription.unsubscribe();
|
|
resolve();
|
|
}, 1000);
|
|
});
|
|
}
|
|
|
|
waitUntilScreensIsRemoved(screenId: AvailableScreens): Promise<void> {
|
|
return new Promise<void>((resolve) => {
|
|
if (!this.isScreenInStack(screenId)) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
const subscription = this.state$.subscribe((state) => {
|
|
if (!state.screenStack.includes(screenId)) {
|
|
subscription.unsubscribe();
|
|
resolve();
|
|
}
|
|
});
|
|
|
|
setTimeout(() => {
|
|
subscription.unsubscribe();
|
|
resolve();
|
|
}, 1000);
|
|
});
|
|
}
|
|
|
|
// Terms of Service state
|
|
isToSOpen(): boolean {
|
|
return this.tosOpen;
|
|
}
|
|
|
|
setToSOpen(open: boolean) {
|
|
this.tosOpen = open;
|
|
}
|
|
|
|
// Private methods
|
|
private extractScreenIds(state: ExpoNavigationState, screenStack: AvailableScreens[]) {
|
|
if (!state) {
|
|
return;
|
|
}
|
|
|
|
const routes = state.routes || [];
|
|
const currentIndex = state.index ?? 0;
|
|
|
|
// Check if this is a tab navigator
|
|
const isTabNavigator = state.type === 'tab';
|
|
|
|
if (isTabNavigator) {
|
|
// For Tab navigators: only process the active tab
|
|
const currentRoute = routes[currentIndex];
|
|
if (currentRoute) {
|
|
const currentScreenId = this.getScreenIdFromRouteKey(currentRoute.key);
|
|
if (currentScreenId && !screenStack.includes(currentScreenId)) {
|
|
screenStack.push(currentScreenId);
|
|
}
|
|
|
|
// Recurse into the active tab's state
|
|
if (currentRoute.state) {
|
|
this.extractScreenIds(currentRoute.state as ExpoNavigationState, screenStack);
|
|
}
|
|
}
|
|
} else {
|
|
// For Stack navigators: process ALL routes in order
|
|
// This captures the full stack including screens underneath overlays
|
|
routes.forEach((route: NavigationRoute<ParamListBase, string>) => {
|
|
const screenId = this.getScreenIdFromRouteKey(route.key);
|
|
if (screenId && !screenStack.includes(screenId)) {
|
|
screenStack.push(screenId);
|
|
}
|
|
|
|
// Recurse into each route's nested state to capture the full hierarchy
|
|
// For non-current routes, this captures underlying screens when overlays are present
|
|
// For the current route, this captures the deepest active screens
|
|
if (route.state) {
|
|
this.extractScreenIds(route.state as ExpoNavigationState, screenStack);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private getScreenIdFromRouteKey(routeKey: string): AvailableScreens | undefined {
|
|
// Route keys in Expo Router are like "(channel_info)/channel_notifications-abc123"
|
|
// or "(channel_info)/index-abc123" or "channel-def456"
|
|
// Extract the route name part before the dash
|
|
const fullPath = routeKey.split('-')[0];
|
|
|
|
// Split by slash to get path segments
|
|
const segments = fullPath.split('/').filter(Boolean);
|
|
|
|
if (segments.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
// Get the last segment
|
|
const lastSegment = segments[segments.length - 1];
|
|
|
|
// If the last segment is 'index', return the parent directory name
|
|
if (lastSegment === 'index' && segments.length > 1) {
|
|
const parentSegment = segments[segments.length - 2];
|
|
|
|
return parentSegment as AvailableScreens;
|
|
}
|
|
|
|
// Otherwise return the last segment
|
|
return lastSegment as AvailableScreens;
|
|
}
|
|
}
|
|
|
|
// Export singleton
|
|
export const NavigationStore = new NavigationStoreSingleton();
|
|
|
|
// React hooks
|
|
export function useCurrentScreen(): AvailableScreens | undefined {
|
|
const [screen, setScreen] = useState<AvailableScreens | undefined>(() => NavigationStore.getVisibleScreen());
|
|
|
|
useEffect(() => {
|
|
const subscription = NavigationStore.currentScreen$.subscribe(setScreen);
|
|
return () => subscription.unsubscribe();
|
|
}, []);
|
|
|
|
return screen;
|
|
}
|