mattermost-mobile/app/screens/navigation.ts
Rajat Dabade e39b27be7e
New keyboard library Setup (#9278)
* 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>
2026-01-13 22:38:35 +05:30

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}});
}