* Initial setup for new keyboard * fix the offset calculation onMove by adding isKeyboardFullyOpened * Done with the keyboard handling implementation * Handled keyboard focus and blured state using context * Added default height for input container * Android support * Tablet state handling * Fix for refreshing offset in list * Created a default context for mention post list * Fix linter errors * Fix tests * Minor * Fix the height issue for tablet view * Review comments * Dependency fix * Reveiw comment * keyboard animation only enabled with screen on top navigation * added tests * Added extra keyboard component for emoji picker (#9328) * Added extra keyboard component * handled swipe geature for extra keyboard * scroll to bottom visible on emoji picker * Check for stale event for keyboard geature area * fix keyboard stale event for mid-gesture change swipe direction * Closing emoji picker when message priority is opened * changing to thread view closes emoji picker * Close emoij picker and keyboard when attachment icons are clicked * handle android emoji picker behaviour * Remove emoji picker code * fix tests * Address reviev comments * Test fixes * usePreventDoubleTab for race condition and corrected comment on isInputAccessoryViewMode flag * File attachments option in bottom sheet quick action (#9331) * Added extra keyboard component * handled swipe geature for extra keyboard * File attachments option in bottom sheet quick action * Updated tests * i18n * fix test * Added tests * Review comment * fix tests * Integrated Emoji picker to Extra keyboard component. (#9339) * Added extra keyboard component * handled swipe geature for extra keyboard * scroll to bottom visible on emoji picker * Fix the post input container height change issue * Added emoji picker with search functionality * keyboard height not recorded fallback to default height, set search to false closing emoji picker * fix search funcitonality in android * scroll to end bottom dismissed with pressed * Fixed the scroll issue for android * fix the flickering post input issue * Wired up the emoji picker * Added keyboard and spaceback in emoji picker * intl extract * initial cursor position and simple calculation review comment * separated grapheme to utils file * Review comments * nitpick * Fix the bottom safe area and navigation stack restore fix * Fix test * disabled emoji picker in edit post screen * change the name of the variable to name it clarier * fix the tab gap issue in andriod * disabled the emoji skin tone change on andriod * fix the input container jump issue * fix the failing test * UX review * added white space after the attachment icon * When @ or / is press open keyboard on andriod also fix the scroll issue when keyboard is open * back bottom closes emoji picker and opening keyboard jump fix * reverted code * fixed the flickering issue of input and scroll position fix * Fix the gap issue in thread * fix the warning reaminated issue when opening a channel * Fix the extra space issue between input and emoij picker * Minor fix for extra space between input and emoji picker * Fix thread view bottom safe area and gap between keyboard and searched emojis * Fix the keyboard issue in tablet * Fix the warning issue react-native-keyboard-controller * Fix tests * Fix the tablet search hight issue * Replaced the ExtraKeyboardProvider with KeyboardProvider from rnkc for permalink * Bottom sheet jump issue resolved * Search do not close after selecting emoji in search list * Added the bottom inset height in search emoji for android * fix buid issue * Fix the cancel state to emoji picker * use portals for autocomplete * fix permalink extra space in post list * Portal only for android --------- Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: Rahim Rahman <rahim.rahman@mattermost.com> Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com> Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
995 lines
30 KiB
TypeScript
995 lines
30 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
/* eslint-disable max-lines */
|
|
|
|
import RNUtils from '@mattermost/rnutils';
|
|
import merge from 'deepmerge';
|
|
import {Appearance, DeviceEventEmitter, Platform, Alert, type EmitterSubscription, Keyboard, StatusBar} from 'react-native';
|
|
import {type ComponentWillAppearEvent, type ImageResource, type LayoutOrientation, Navigation, type Options, OptionsModalPresentationStyle, type OptionsTopBarButton, type ScreenPoppedEvent, type EventSubscription} from 'react-native-navigation';
|
|
import tinyColor from 'tinycolor2';
|
|
|
|
import CompassIcon from '@components/compass_icon';
|
|
import {Events, Screens, Launch} from '@constants';
|
|
import {NOT_READY} from '@constants/screens';
|
|
import {getDefaultThemeByAppearance} from '@context/theme';
|
|
import EphemeralStore from '@store/ephemeral_store';
|
|
import NavigationStore from '@store/navigation_store';
|
|
import {isTablet} from '@utils/helpers';
|
|
import {logError} from '@utils/log';
|
|
import {appearanceControlledScreens, mergeNavigationOptions} from '@utils/navigation';
|
|
import {changeOpacity, setNavigatorStyles} from '@utils/theme';
|
|
|
|
import type {BottomSheetFooterProps} from '@gorhom/bottom-sheet';
|
|
import type {default as UserProfileScreen} from '@screens/user_profile';
|
|
import type {LaunchProps} from '@typings/launch';
|
|
import type {AvailableScreens, NavButtons} from '@typings/screens/navigation';
|
|
import type {ComponentProps} from 'react';
|
|
import type {IntlShape} from 'react-intl';
|
|
import type {Asset} from 'react-native-image-picker';
|
|
|
|
const alpha = {
|
|
from: 0,
|
|
to: 1,
|
|
duration: 150,
|
|
};
|
|
let subscriptions: Array<EmitterSubscription | EventSubscription> | undefined;
|
|
|
|
export const allOrientations: LayoutOrientation[] = ['sensor', 'sensorLandscape', 'sensorPortrait', 'landscape', 'portrait'];
|
|
export const portraitOrientation: LayoutOrientation[] = ['portrait'];
|
|
|
|
const loginFlowScreens = new Set<AvailableScreens>([
|
|
Screens.ONBOARDING,
|
|
Screens.SERVER,
|
|
Screens.LOGIN,
|
|
Screens.SSO,
|
|
Screens.MFA,
|
|
Screens.FORGOT_PASSWORD,
|
|
]);
|
|
|
|
function setNavigationBarColor(screen: AvailableScreens, th?: Theme) {
|
|
if (Platform.OS === 'android' && Platform.Version >= 34) {
|
|
const theme = th || getThemeFromState();
|
|
const color = loginFlowScreens.has(screen) ? theme.sidebarBg : theme.centerChannelBg;
|
|
RNUtils.setNavigationBarColor(color, tinyColor(color).isLight());
|
|
}
|
|
}
|
|
|
|
function showBottomTabsIfNeeded(screen: AvailableScreens) {
|
|
if (screen === Screens.HOME) {
|
|
DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, true);
|
|
}
|
|
}
|
|
|
|
export function registerNavigationListeners() {
|
|
subscriptions?.forEach((v) => v.remove());
|
|
subscriptions = [
|
|
Navigation.events().registerScreenPoppedListener(onPoppedListener),
|
|
Navigation.events().registerCommandListener(onCommandListener),
|
|
Navigation.events().registerComponentWillAppearListener(onScreenWillAppear),
|
|
|
|
/**
|
|
* For the time being and until we add the emoji picker in the keyboard area
|
|
* will keep Android as adjustResize cause useAnimatedKeyboard from reanimated
|
|
* is reporting the wrong values when the keyboard was opened but we switch
|
|
* to a different channel or thread.
|
|
*/
|
|
// Navigation.events().registerComponentDidAppearListener(onScreenDidAppear),
|
|
// Navigation.events().registerComponentDidDisappearListener(onScreenDidDisappear),
|
|
];
|
|
}
|
|
|
|
function onCommandListener(name: string, params: any) {
|
|
switch (name) {
|
|
case 'setRoot':
|
|
NavigationStore.clearScreensFromStack();
|
|
NavigationStore.addScreenToStack(params.layout.root.children[0].id);
|
|
break;
|
|
case 'push':
|
|
NavigationStore.addScreenToStack(params.layout.id);
|
|
break;
|
|
case 'showModal':
|
|
NavigationStore.addModalToStack(params.layout.children[0].id);
|
|
break;
|
|
case 'popToRoot':
|
|
NavigationStore.clearScreensFromStack();
|
|
NavigationStore.addScreenToStack(Screens.HOME);
|
|
break;
|
|
case 'popTo':
|
|
NavigationStore.popTo(params.componentId);
|
|
break;
|
|
case 'dismissModal':
|
|
NavigationStore.removeModalFromStack(params.componentId);
|
|
break;
|
|
}
|
|
|
|
const screen = NavigationStore.getVisibleScreen();
|
|
showBottomTabsIfNeeded(screen);
|
|
setNavigationBarColor(screen);
|
|
}
|
|
|
|
function onPoppedListener({componentId}: ScreenPoppedEvent) {
|
|
// screen pop does not trigger registerCommandListener, but does trigger screenPoppedListener
|
|
const screen = componentId as AvailableScreens;
|
|
NavigationStore.removeScreenFromStack(screen);
|
|
|
|
// If we pop to home, we need to show the tab bar
|
|
showBottomTabsIfNeeded(NavigationStore.getVisibleScreen());
|
|
|
|
setNavigationBarColor(screen);
|
|
}
|
|
|
|
function onScreenWillAppear(event: ComponentWillAppearEvent) {
|
|
showBottomTabsIfNeeded(event.componentId as AvailableScreens);
|
|
}
|
|
|
|
export const loginAnimationOptions = () => {
|
|
const theme = getThemeFromState();
|
|
return {
|
|
layout: {
|
|
backgroundColor: theme.centerChannelBg,
|
|
componentBackgroundColor: theme.centerChannelBg,
|
|
},
|
|
topBar: {
|
|
visible: true,
|
|
drawBehind: true,
|
|
translucid: true,
|
|
noBorder: true,
|
|
elevation: 0,
|
|
background: {
|
|
color: 'transparent',
|
|
},
|
|
backButton: {
|
|
color: changeOpacity(theme.centerChannelColor, 0.56),
|
|
},
|
|
scrollEdgeAppearance: {
|
|
active: true,
|
|
noBorder: true,
|
|
translucid: true,
|
|
},
|
|
},
|
|
animations: {
|
|
topBar: {
|
|
alpha,
|
|
},
|
|
push: {
|
|
waitForRender: false,
|
|
content: {
|
|
alpha,
|
|
},
|
|
},
|
|
pop: {
|
|
content: {
|
|
alpha: {
|
|
from: 1,
|
|
to: 0,
|
|
duration: 100,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
};
|
|
|
|
export const bottomSheetModalOptions = (theme: Theme, closeButtonId?: string): Options => {
|
|
if (closeButtonId) {
|
|
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.centerChannelColor);
|
|
const closeButtonTestId = `${closeButtonId.replace('close-', 'close.').replace(/-/g, '_')}.button`;
|
|
return {
|
|
modalPresentationStyle: OptionsModalPresentationStyle.formSheet,
|
|
topBar: {
|
|
leftButtons: [{
|
|
id: closeButtonId,
|
|
icon: closeButton,
|
|
testID: closeButtonTestId,
|
|
}],
|
|
leftButtonColor: changeOpacity(theme.centerChannelColor, 0.56),
|
|
background: {
|
|
color: theme.centerChannelBg,
|
|
},
|
|
title: {
|
|
color: theme.centerChannelColor,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
animations: {
|
|
showModal: {
|
|
enabled: false,
|
|
},
|
|
dismissModal: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
modalPresentationStyle: Platform.select({
|
|
ios: OptionsModalPresentationStyle.overFullScreen,
|
|
default: OptionsModalPresentationStyle.overCurrentContext,
|
|
}),
|
|
statusBar: {
|
|
backgroundColor: theme.sidebarBg,
|
|
drawBehind: true,
|
|
translucent: true,
|
|
},
|
|
};
|
|
};
|
|
|
|
// This locks phones to portrait for all screens while keeps
|
|
// all orientations available for Tablets.
|
|
Navigation.setDefaultOptions({
|
|
animations: {
|
|
setRoot: {
|
|
enter: {
|
|
waitForRender: true,
|
|
enabled: true,
|
|
alpha: {
|
|
from: 0,
|
|
to: 1,
|
|
duration: 300,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
layout: {
|
|
orientation: isTablet() ? allOrientations : portraitOrientation,
|
|
},
|
|
topBar: {
|
|
title: {
|
|
fontFamily: 'Metropolis-SemiBold',
|
|
fontSize: 18,
|
|
fontWeight: '600',
|
|
},
|
|
backButton: {
|
|
enableMenu: false,
|
|
},
|
|
subtitle: {
|
|
fontFamily: 'OpenSans',
|
|
fontSize: 12,
|
|
fontWeight: '400',
|
|
},
|
|
},
|
|
});
|
|
|
|
Appearance.addChangeListener(() => {
|
|
const theme = getThemeFromState();
|
|
const screens = NavigationStore.getScreensInStack();
|
|
|
|
if (screens.includes(Screens.SERVER) || screens.includes(Screens.ONBOARDING)) {
|
|
for (const screen of screens) {
|
|
if (appearanceControlledScreens.has(screen)) {
|
|
Navigation.updateProps(screen, {theme});
|
|
setNavigatorStyles(screen, theme, loginAnimationOptions(), theme.sidebarBg);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
export function setScreensOrientation(allowRotation: boolean) {
|
|
const options: Options = {
|
|
layout: {
|
|
orientation: allowRotation ? allOrientations : portraitOrientation,
|
|
},
|
|
};
|
|
Navigation.setDefaultOptions(options);
|
|
const screens = NavigationStore.getScreensInStack();
|
|
for (const s of screens) {
|
|
Navigation.mergeOptions(s, options);
|
|
}
|
|
}
|
|
|
|
export function getThemeFromState(): Theme {
|
|
return EphemeralStore.theme || getDefaultThemeByAppearance();
|
|
}
|
|
|
|
// This is a temporary helper function to avoid
|
|
// crashes when trying to load a screen that does
|
|
// NOT exists, this should be removed for GA
|
|
function isScreenRegistered(screen: AvailableScreens) {
|
|
const notImplemented = NOT_READY.includes(screen) || !Object.values(Screens).includes(screen);
|
|
if (notImplemented) {
|
|
Alert.alert(
|
|
'Temporary error ' + screen,
|
|
'The functionality you are trying to use has not been implemented yet',
|
|
);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function edgeToEdgeHack(screen: AvailableScreens, theme: Theme) {
|
|
const isDark = tinyColor(theme.sidebarBg).isDark();
|
|
|
|
if (Platform.OS === 'android') {
|
|
if (Platform.Version >= 34) {
|
|
const listener = Navigation.events().registerComponentDidAppearListener((event) => {
|
|
if (event.componentName === screen) {
|
|
setNavigationBarColor(screen, theme);
|
|
listener.remove();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (Platform.Version >= 36) {
|
|
return {
|
|
drawBehind: true,
|
|
translucent: false,
|
|
isDark,
|
|
};
|
|
}
|
|
}
|
|
|
|
StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
|
|
return {isDark};
|
|
}
|
|
|
|
export function openToS() {
|
|
NavigationStore.setToSOpen(true);
|
|
return showOverlay(Screens.TERMS_OF_SERVICE, {}, {overlay: {interceptTouchOutside: true}});
|
|
}
|
|
|
|
export function resetToHome(passProps: LaunchProps = {launchType: Launch.Normal}) {
|
|
const theme = getThemeFromState();
|
|
const edgeToEdge = edgeToEdgeHack(Screens.HOME, theme);
|
|
|
|
if (!passProps.coldStart && (passProps.launchType === Launch.AddServer || passProps.launchType === Launch.AddServerFromDeepLink)) {
|
|
dismissModal({componentId: Screens.SERVER});
|
|
dismissModal({componentId: Screens.LOGIN});
|
|
dismissModal({componentId: Screens.SSO});
|
|
dismissModal({componentId: Screens.BOTTOM_SHEET});
|
|
if (passProps.launchType === Launch.AddServerFromDeepLink) {
|
|
Navigation.updateProps(Screens.HOME, {launchType: Launch.DeepLink, extra: passProps.extra});
|
|
}
|
|
return '';
|
|
}
|
|
|
|
const stack = {
|
|
children: [{
|
|
component: {
|
|
id: Screens.HOME,
|
|
name: Screens.HOME,
|
|
passProps,
|
|
options: {
|
|
layout: {
|
|
componentBackgroundColor: theme.centerChannelBg,
|
|
},
|
|
statusBar: {
|
|
visible: true,
|
|
backgroundColor: theme.sidebarBg,
|
|
...edgeToEdge,
|
|
},
|
|
topBar: {
|
|
visible: false,
|
|
height: 0,
|
|
background: {
|
|
color: theme.sidebarBg,
|
|
},
|
|
backButton: {
|
|
visible: false,
|
|
color: theme.sidebarHeaderTextColor,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}],
|
|
};
|
|
|
|
return Navigation.setRoot({
|
|
root: {stack},
|
|
});
|
|
}
|
|
|
|
export function resetToSelectServer(passProps: LaunchProps) {
|
|
const theme = getDefaultThemeByAppearance();
|
|
const edgeToEdge = edgeToEdgeHack(Screens.SERVER, theme);
|
|
|
|
const children = [{
|
|
component: {
|
|
id: Screens.SERVER,
|
|
name: Screens.SERVER,
|
|
passProps: {
|
|
...passProps,
|
|
theme,
|
|
},
|
|
options: {
|
|
layout: {
|
|
backgroundColor: theme.centerChannelBg,
|
|
componentBackgroundColor: theme.centerChannelBg,
|
|
},
|
|
statusBar: {
|
|
visible: true,
|
|
backgroundColor: theme.sidebarBg,
|
|
...edgeToEdge,
|
|
},
|
|
topBar: {
|
|
backButton: {
|
|
color: theme.sidebarHeaderTextColor,
|
|
title: '',
|
|
},
|
|
background: {
|
|
color: theme.sidebarBg,
|
|
},
|
|
visible: false,
|
|
height: 0,
|
|
},
|
|
},
|
|
},
|
|
}];
|
|
|
|
return Navigation.setRoot({
|
|
root: {
|
|
stack: {
|
|
children,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
export function resetToOnboarding(passProps: LaunchProps) {
|
|
const theme = getDefaultThemeByAppearance();
|
|
const edgeToEdge = edgeToEdgeHack(Screens.ONBOARDING, theme);
|
|
|
|
const children = [{
|
|
component: {
|
|
id: Screens.ONBOARDING,
|
|
name: Screens.ONBOARDING,
|
|
passProps: {
|
|
...passProps,
|
|
theme,
|
|
},
|
|
options: {
|
|
layout: {
|
|
backgroundColor: theme.centerChannelBg,
|
|
componentBackgroundColor: theme.centerChannelBg,
|
|
},
|
|
statusBar: {
|
|
visible: true,
|
|
backgroundColor: theme.sidebarBg,
|
|
...edgeToEdge,
|
|
},
|
|
topBar: {
|
|
backButton: {
|
|
color: theme.sidebarHeaderTextColor,
|
|
title: '',
|
|
},
|
|
background: {
|
|
color: theme.sidebarBg,
|
|
},
|
|
visible: false,
|
|
height: 0,
|
|
},
|
|
},
|
|
},
|
|
}];
|
|
|
|
return Navigation.setRoot({
|
|
root: {
|
|
stack: {
|
|
children,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
export function resetToTeams() {
|
|
const theme = getThemeFromState();
|
|
const edgeToEdge = edgeToEdgeHack(Screens.SELECT_TEAM, theme);
|
|
|
|
return Navigation.setRoot({
|
|
root: {
|
|
stack: {
|
|
children: [{
|
|
component: {
|
|
id: Screens.SELECT_TEAM,
|
|
name: Screens.SELECT_TEAM,
|
|
options: {
|
|
layout: {
|
|
componentBackgroundColor: theme.centerChannelBg,
|
|
},
|
|
statusBar: {
|
|
visible: true,
|
|
backgroundColor: theme.sidebarBg,
|
|
...edgeToEdge,
|
|
},
|
|
topBar: {
|
|
visible: false,
|
|
height: 0,
|
|
background: {
|
|
color: theme.sidebarBg,
|
|
},
|
|
backButton: {
|
|
visible: false,
|
|
color: theme.sidebarHeaderTextColor,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}],
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
export function goToScreen(name: AvailableScreens, title: string, passProps = {}, options: Options = {}) {
|
|
if (!isScreenRegistered(name)) {
|
|
return '';
|
|
}
|
|
|
|
const theme = getThemeFromState();
|
|
const edgeToEdge = edgeToEdgeHack(name, theme);
|
|
const componentId = NavigationStore.getVisibleScreen();
|
|
if (!componentId) {
|
|
logError('Trying to go to screen without any screen on the navigation store');
|
|
return '';
|
|
}
|
|
|
|
const defaultOptions: Options = {
|
|
layout: {
|
|
componentBackgroundColor: theme.centerChannelBg,
|
|
},
|
|
popGesture: true,
|
|
sideMenu: {
|
|
left: {enabled: false},
|
|
right: {enabled: false},
|
|
},
|
|
statusBar: {
|
|
style: edgeToEdge.isDark ? 'light' : 'dark',
|
|
backgroundColor: theme.sidebarBg,
|
|
drawBehind: edgeToEdge.drawBehind ?? false,
|
|
translucent: edgeToEdge.translucent ?? false,
|
|
},
|
|
topBar: {
|
|
animate: true,
|
|
visible: true,
|
|
backButton: {
|
|
color: theme.sidebarHeaderTextColor,
|
|
title: '',
|
|
testID: 'screen.back.button',
|
|
},
|
|
background: {
|
|
color: theme.sidebarBg,
|
|
},
|
|
title: {
|
|
color: theme.sidebarHeaderTextColor,
|
|
text: title,
|
|
},
|
|
},
|
|
};
|
|
|
|
DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, false);
|
|
|
|
if (NavigationStore.getScreensInStack().includes(name)) {
|
|
Navigation.updateProps(name, passProps);
|
|
return Navigation.popTo(name, merge(defaultOptions, options));
|
|
}
|
|
|
|
return Navigation.push(componentId, {
|
|
component: {
|
|
id: name,
|
|
name,
|
|
passProps,
|
|
options: merge(defaultOptions, options),
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function popTopScreen(screenId?: AvailableScreens) {
|
|
try {
|
|
if (screenId) {
|
|
await Navigation.pop(screenId);
|
|
} else {
|
|
const componentId = NavigationStore.getVisibleScreen();
|
|
await Navigation.pop(componentId);
|
|
}
|
|
} catch (error) {
|
|
// RNN returns a promise rejection if there are no screens
|
|
// atop the root screen to pop. We'll do nothing in this case.
|
|
}
|
|
}
|
|
|
|
export async function popTo(screenId: AvailableScreens) {
|
|
try {
|
|
await Navigation.popTo(screenId);
|
|
} catch (error) {
|
|
// RNN returns a promise rejection if there are no screens
|
|
// atop the root screen to pop. We'll do nothing in this case.
|
|
}
|
|
}
|
|
|
|
export async function popToRoot() {
|
|
const componentId = NavigationStore.getVisibleScreen();
|
|
|
|
try {
|
|
await Navigation.popToRoot(componentId);
|
|
} catch (error) {
|
|
// RNN returns a promise rejection if there are no screens
|
|
// atop the root screen to pop. We'll do nothing in this case.
|
|
}
|
|
}
|
|
|
|
export async function dismissAllModalsAndPopToRoot() {
|
|
await dismissAllModals();
|
|
await dismissAllOverlays();
|
|
await popToRoot();
|
|
}
|
|
|
|
/**
|
|
* Dismisses All modals in the stack and pops the stack to the desired screen
|
|
* (if the screen is not in the stack, it will push a new one)
|
|
* @param screenId Screen to pop or display
|
|
* @param title Title to be shown in the top bar
|
|
* @param passProps Props to pass to the screen
|
|
* @param options Navigation options
|
|
*/
|
|
export async function dismissAllModalsAndPopToScreen(screenId: AvailableScreens, title: string, passProps = {}, options = {}) {
|
|
await dismissAllModals();
|
|
await dismissAllOverlays();
|
|
if (NavigationStore.getScreensInStack().includes(screenId)) {
|
|
let mergeOptions = options;
|
|
if (title) {
|
|
mergeOptions = merge(mergeOptions, {
|
|
topBar: {
|
|
title: {
|
|
text: title,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
try {
|
|
await Navigation.popTo(screenId, mergeOptions);
|
|
if (Object.keys(passProps).length > 0) {
|
|
Navigation.updateProps(screenId, passProps);
|
|
}
|
|
} catch {
|
|
// catch in case there is nothing to pop
|
|
}
|
|
} else {
|
|
await goToScreen(screenId, title, passProps, options);
|
|
}
|
|
}
|
|
|
|
export function showModal(name: AvailableScreens, title: string, passProps = {}, options: Options = {}) {
|
|
if (!isScreenRegistered(name) || NavigationStore.getVisibleModal() === name) {
|
|
return;
|
|
}
|
|
|
|
const theme = getThemeFromState();
|
|
const edgeToEdge = edgeToEdgeHack(name, theme);
|
|
const modalPresentationStyle: OptionsModalPresentationStyle = Platform.OS === 'ios' ? OptionsModalPresentationStyle.pageSheet : OptionsModalPresentationStyle.none;
|
|
const defaultOptions: Options = {
|
|
modalPresentationStyle,
|
|
layout: {
|
|
componentBackgroundColor: theme.centerChannelBg,
|
|
},
|
|
statusBar: {
|
|
visible: true,
|
|
backgroundColor: theme.sidebarBg,
|
|
...edgeToEdge,
|
|
},
|
|
topBar: {
|
|
animate: true,
|
|
visible: true,
|
|
backButton: {
|
|
color: theme.sidebarHeaderTextColor,
|
|
title: '',
|
|
},
|
|
background: {
|
|
color: theme.sidebarBg,
|
|
},
|
|
title: {
|
|
color: theme.sidebarHeaderTextColor,
|
|
text: title,
|
|
},
|
|
leftButtonColor: theme.sidebarHeaderTextColor,
|
|
rightButtonColor: theme.sidebarHeaderTextColor,
|
|
},
|
|
modal: {swipeToDismiss: false},
|
|
};
|
|
|
|
Navigation.showModal({
|
|
stack: {
|
|
children: [{
|
|
component: {
|
|
id: name,
|
|
name,
|
|
passProps: {
|
|
...passProps,
|
|
isModal: true,
|
|
},
|
|
options: merge(defaultOptions, options),
|
|
},
|
|
}],
|
|
},
|
|
});
|
|
}
|
|
|
|
export function showModalOverCurrentContext(name: AvailableScreens, passProps = {}, options: Options = {}) {
|
|
const title = '';
|
|
let animations;
|
|
switch (Platform.OS) {
|
|
case 'android':
|
|
animations = {
|
|
showModal: {
|
|
waitForRender: false,
|
|
alpha: {
|
|
from: 0,
|
|
to: 1,
|
|
duration: 250,
|
|
},
|
|
},
|
|
dismissModal: {
|
|
alpha: {
|
|
from: 1,
|
|
to: 0,
|
|
duration: 250,
|
|
},
|
|
},
|
|
};
|
|
break;
|
|
default:
|
|
animations = {
|
|
showModal: {
|
|
alpha: {
|
|
from: 0,
|
|
to: 1,
|
|
duration: 250,
|
|
},
|
|
},
|
|
dismissModal: {
|
|
enter: {
|
|
enabled: false,
|
|
},
|
|
exit: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
};
|
|
break;
|
|
}
|
|
const defaultOptions = {
|
|
modalPresentationStyle: OptionsModalPresentationStyle.overCurrentContext,
|
|
layout: {
|
|
backgroundColor: 'transparent',
|
|
componentBackgroundColor: 'transparent',
|
|
},
|
|
topBar: {
|
|
visible: false,
|
|
height: 0,
|
|
},
|
|
animations,
|
|
};
|
|
const mergeOptions = merge(defaultOptions, options);
|
|
showModal(name, title, passProps, mergeOptions);
|
|
}
|
|
|
|
export async function dismissModal(options?: Options & { componentId: AvailableScreens}) {
|
|
if (!NavigationStore.hasModalsOpened()) {
|
|
return;
|
|
}
|
|
|
|
const componentId = options?.componentId || NavigationStore.getVisibleModal();
|
|
if (componentId) {
|
|
try {
|
|
await Navigation.dismissModal(componentId, options);
|
|
} catch (error) {
|
|
// RNN returns a promise rejection if there is no modal to
|
|
// dismiss. We'll do nothing in this case.
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function dismissAllModals() {
|
|
if (!NavigationStore.hasModalsOpened()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const modals = [...NavigationStore.getModalsInStack()];
|
|
for await (const modal of modals) {
|
|
await Navigation.dismissModal(modal, {animations: {dismissModal: {enabled: false}}});
|
|
}
|
|
} catch (error) {
|
|
// RNN returns a promise rejection if there are no modals to
|
|
// dismiss. We'll do nothing in this case.
|
|
}
|
|
}
|
|
|
|
export const buildNavigationButton = (id: string, testID: string, icon?: ImageResource, text?: string): OptionsTopBarButton => ({
|
|
fontSize: 16,
|
|
fontFamily: 'OpenSans-SemiBold',
|
|
fontWeight: '600',
|
|
id,
|
|
icon,
|
|
showAsAction: 'always',
|
|
testID,
|
|
text,
|
|
});
|
|
|
|
export function setButtons(componentId: AvailableScreens, buttons: NavButtons = {leftButtons: [], rightButtons: []}) {
|
|
const options = {
|
|
topBar: {
|
|
...buttons,
|
|
},
|
|
};
|
|
|
|
mergeNavigationOptions(componentId, options);
|
|
}
|
|
|
|
export function showOverlay(name: AvailableScreens, passProps = {}, options: Options = {}, id?: string) {
|
|
if (!isScreenRegistered(name)) {
|
|
return;
|
|
}
|
|
|
|
const defaultOptions = {
|
|
layout: {
|
|
backgroundColor: 'transparent',
|
|
componentBackgroundColor: 'transparent',
|
|
},
|
|
overlay: {
|
|
interceptTouchOutside: false,
|
|
},
|
|
};
|
|
|
|
Navigation.showOverlay({
|
|
component: {
|
|
id,
|
|
name,
|
|
passProps,
|
|
options: merge(defaultOptions, options),
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function dismissOverlay(componentId: string) {
|
|
try {
|
|
await Navigation.dismissOverlay(componentId);
|
|
} catch (error) {
|
|
// RNN returns a promise rejection if there is no modal with
|
|
// this componentId to dismiss. We'll do nothing in this case.
|
|
}
|
|
}
|
|
|
|
export async function dismissAllOverlays() {
|
|
try {
|
|
await Navigation.dismissAllOverlays();
|
|
} catch {
|
|
// do nothing
|
|
}
|
|
}
|
|
|
|
type BottomSheetArgs = {
|
|
closeButtonId: string;
|
|
initialSnapIndex?: number;
|
|
footerComponent?: React.FC<BottomSheetFooterProps>;
|
|
renderContent: () => React.ReactNode;
|
|
snapPoints: Array<number | string>;
|
|
theme: Theme;
|
|
title: string;
|
|
scrollable?: boolean;
|
|
}
|
|
|
|
export function bottomSheet({title, renderContent, footerComponent, snapPoints, initialSnapIndex = 1, theme, closeButtonId, scrollable = false}: BottomSheetArgs) {
|
|
if (isTablet()) {
|
|
showModal(Screens.BOTTOM_SHEET, title, {
|
|
closeButtonId,
|
|
initialSnapIndex,
|
|
renderContent,
|
|
footerComponent,
|
|
snapPoints,
|
|
scrollable,
|
|
}, bottomSheetModalOptions(theme, closeButtonId));
|
|
} else {
|
|
showModalOverCurrentContext(Screens.BOTTOM_SHEET, {
|
|
initialSnapIndex,
|
|
renderContent,
|
|
footerComponent,
|
|
snapPoints,
|
|
scrollable,
|
|
}, bottomSheetModalOptions(theme));
|
|
}
|
|
}
|
|
|
|
export async function dismissBottomSheet(alternativeScreen: AvailableScreens = Screens.BOTTOM_SHEET) {
|
|
DeviceEventEmitter.emit(Events.CLOSE_BOTTOM_SHEET);
|
|
await NavigationStore.waitUntilScreensIsRemoved(alternativeScreen);
|
|
}
|
|
|
|
type AsBottomSheetArgs = {
|
|
closeButtonId: string;
|
|
props?: Record<string, any>;
|
|
screen: AvailableScreens;
|
|
theme: Theme;
|
|
title: string;
|
|
}
|
|
|
|
export function openAsBottomSheet({closeButtonId, screen, theme, title, props}: AsBottomSheetArgs) {
|
|
if (isTablet()) {
|
|
showModal(screen, title, {
|
|
closeButtonId,
|
|
...props,
|
|
}, bottomSheetModalOptions(theme, closeButtonId));
|
|
} else {
|
|
showModalOverCurrentContext(screen, props, bottomSheetModalOptions(theme));
|
|
}
|
|
}
|
|
|
|
export function openAttachmentOptions(
|
|
intl: IntlShape,
|
|
theme: Theme,
|
|
props: {
|
|
onUploadFiles: (files: Asset[]) => void;
|
|
maxFilesReached: boolean;
|
|
canUploadFiles: boolean;
|
|
testID?: string;
|
|
fileCount?: number;
|
|
maxFileCount?: number;
|
|
},
|
|
) {
|
|
const title = intl.formatMessage({id: 'mobile.file_attachment.title', defaultMessage: 'Files and media'});
|
|
openAsBottomSheet({
|
|
closeButtonId: 'attachment-close-id',
|
|
screen: Screens.ATTACHMENT_OPTIONS,
|
|
theme,
|
|
title,
|
|
props,
|
|
});
|
|
}
|
|
|
|
export const showAppForm = async (form: AppForm, context: AppContext) => {
|
|
const passProps = {form, context};
|
|
showModal(Screens.APPS_FORM, form.title || '', passProps);
|
|
};
|
|
|
|
export const showReviewOverlay = (hasAskedBefore: boolean) => {
|
|
showOverlay(
|
|
Screens.REVIEW_APP,
|
|
{hasAskedBefore},
|
|
{overlay: {interceptTouchOutside: true}},
|
|
);
|
|
};
|
|
|
|
export const showShareFeedbackOverlay = () => {
|
|
showOverlay(
|
|
Screens.SHARE_FEEDBACK,
|
|
{},
|
|
{overlay: {interceptTouchOutside: true}},
|
|
);
|
|
};
|
|
|
|
export async function findChannels(title: string, theme: Theme) {
|
|
const options: Options = {};
|
|
const closeButtonId = 'close-find-channels';
|
|
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
|
options.topBar = {
|
|
leftButtons: [{
|
|
id: closeButtonId,
|
|
icon: closeButton,
|
|
testID: 'close.find_channels.button',
|
|
}],
|
|
};
|
|
|
|
showModal(
|
|
Screens.FIND_CHANNELS,
|
|
title,
|
|
{closeButtonId},
|
|
options,
|
|
);
|
|
}
|
|
|
|
export async function openUserProfileModal(
|
|
intl: IntlShape,
|
|
theme: Theme,
|
|
props: Omit<ComponentProps<typeof UserProfileScreen>, 'closeButtonId'>,
|
|
screenToDismiss?: AvailableScreens,
|
|
) {
|
|
if (screenToDismiss) {
|
|
await dismissBottomSheet(screenToDismiss);
|
|
}
|
|
const screen = Screens.USER_PROFILE;
|
|
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
|
|
const closeButtonId = 'close-user-profile';
|
|
|
|
Keyboard.dismiss();
|
|
openAsBottomSheet({screen, title, theme, closeButtonId, props: {...props}});
|
|
}
|