Refactor Open Profile (#8746)

* Refactor Open Profile

* Add missing changes

* Fix tests

* Fix tests

* Address feedback
This commit is contained in:
Daniel Espino García 2025-05-13 17:07:13 +02:00 committed by GitHub
parent 305fbee513
commit af0a7525c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
89 changed files with 509 additions and 270 deletions

View file

@ -23,6 +23,7 @@ import {secureGetFromRecord} from '@utils/types';
import {displayUsername} from '@utils/user';
import type {WithDatabaseArgs} from '@typings/database/database';
import type {AvailableScreens} from '@typings/screens/navigation';
type Selection = DialogOption | Channel | UserProfile | DialogOption[] | Channel[] | UserProfile[];
@ -43,6 +44,7 @@ type AutoCompleteSelectorProps = {
teammateNameDisplay: string;
isMultiselect?: boolean;
testID: string;
location: AvailableScreens;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
@ -131,8 +133,22 @@ function getTextAndValueFromSelectedItem(item: Selection, teammateNameDisplay: s
}
function AutoCompleteSelector({
dataSource, disabled = false, errorText, getDynamicOptions, helpText, label, onSelected, optional = false,
options, placeholder, roundedBorders = true, selected, teammateNameDisplay, isMultiselect = false, testID,
dataSource,
disabled = false,
errorText,
getDynamicOptions,
helpText,
label,
onSelected,
optional = false,
options,
placeholder,
roundedBorders = true,
selected,
teammateNameDisplay,
isMultiselect = false,
testID,
location,
}: AutoCompleteSelectorProps) {
const intl = useIntl();
const theme = useTheme();
@ -221,6 +237,7 @@ function AutoCompleteSelector({
disabled={disabled}
helpText={helpText}
errorText={errorText}
location={location}
/>
</View>
);

View file

@ -23,10 +23,11 @@ import type ChannelModel from '@typings/database/models/servers/channel';
import type DraftModel from '@typings/database/models/servers/draft';
import type ScheduledPostModel from '@typings/database/models/servers/scheduled_post';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
channel: ChannelModel;
location: string;
location: AvailableScreens;
postReceiverUser?: UserModel;
post: DraftModel | ScheduledPostModel;
layoutWidth: number;

View file

@ -16,11 +16,12 @@ import {typography} from '@utils/typography';
import type DraftModel from '@typings/database/models/servers/draft';
import type ScheduledPostModel from '@typings/database/models/servers/scheduled_post';
import type {UserMentionKey} from '@typings/global/markdown';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
post: DraftModel | ScheduledPostModel;
layoutWidth: number;
location: string;
location: AvailableScreens;
}
const EMPTY_MENTION_KEYS: UserMentionKey[] = [];

View file

@ -12,10 +12,11 @@ import DraftAndScheduledPostMessage from './draft_and_scheduled_post_message';
import type DraftModel from '@typings/database/models/servers/draft';
import type ScheduledPostModel from '@typings/database/models/servers/scheduled_post';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
post: DraftModel | ScheduledPostModel;
location: string;
location: AvailableScreens;
layoutWidth: number;
}

View file

@ -14,6 +14,7 @@ import {logWarning} from '@utils/log';
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
import {concatStyles, changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import type {AvailableScreens} from '@typings/screens/navigation';
import type {PrimitiveType} from 'intl-messageformat';
type Props = {
@ -21,7 +22,7 @@ type Props = {
channelId?: string;
defaultMessage: string;
id: string;
location: string;
location: AvailableScreens;
onPostPress?: (e: GestureResponderEvent) => void;
style?: StyleProp<TextStyle>;
values?: Record<string, PrimitiveType>;

View file

@ -5,28 +5,28 @@ import {useManagedConfig} from '@mattermost/react-native-emm';
import Clipboard from '@react-native-clipboard/clipboard';
import React, {useCallback, useEffect, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {type GestureResponderEvent, Keyboard, type StyleProp, StyleSheet, Text, type TextStyle, View} from 'react-native';
import {type GestureResponderEvent, type StyleProp, StyleSheet, Text, type TextStyle, View} from 'react-native';
import {fetchUserOrGroupsByMentionsInBatch} from '@actions/remote/user';
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import GroupModel from '@database/models/server/group';
import {useMemoMentionedGroup, useMemoMentionedUser} from '@hooks/markdown';
import {bottomSheet, dismissBottomSheet, openAsBottomSheet} from '@screens/navigation';
import {bottomSheet, dismissBottomSheet, openUserProfileModal} from '@screens/navigation';
import {bottomSheetSnapPoint} from '@utils/helpers';
import {displayUsername} from '@utils/user';
import type GroupMembershipModel from '@typings/database/models/servers/group_membership';
import type UserModelType from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
type AtMentionProps = {
channelId?: string;
currentUserId: string;
disableAtChannelMentionHighlight?: boolean;
isSearchResult?: boolean;
location: string;
location: AvailableScreens;
mentionKeys?: Array<{key: string }>;
mentionName: string;
mentionStyle: StyleProp<TextStyle>;
@ -95,13 +95,11 @@ const AtMention = ({
return;
}
const screen = Screens.USER_PROFILE;
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const closeButtonId = 'close-user-profile';
const props = {closeButtonId, location, userId: user.id, channelId};
Keyboard.dismiss();
openAsBottomSheet({screen, title, theme, closeButtonId, props});
openUserProfileModal(intl, theme, {
location,
userId: user.id,
channelId,
});
};
const handleLongPress = useCallback(() => {

View file

@ -19,7 +19,7 @@ describe('Markdown', () => {
baseTextStyle: {},
enableInlineLatex: true,
enableLatex: true,
location: 'somewhere?',
location: 'Channel',
maxNodes: 2000,
theme: Preferences.THEMES.denim,
};

View file

@ -38,6 +38,7 @@ import type {
MarkdownAtMentionRenderer, MarkdownBaseRenderer, MarkdownBlockStyles, MarkdownChannelMentionRenderer,
MarkdownEmojiRenderer, MarkdownImageRenderer, MarkdownLatexRenderer, MarkdownTextStyles, SearchPattern, UserMentionKey, HighlightWithoutNotificationKey,
} from '@typings/global/markdown';
import type {AvailableScreens} from '@typings/screens/navigation';
type MarkdownProps = {
autolinkedUrlSchemes?: string[];
@ -65,7 +66,7 @@ type MarkdownProps = {
isSearchResult?: boolean;
layoutHeight?: number;
layoutWidth?: number;
location: string;
location: AvailableScreens;
maxNodes: number;
mentionKeys?: UserMentionKey[];
minimumHashtagLength?: number;

View file

@ -16,9 +16,12 @@ import {popToRoot} from '@screens/navigation';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
testID?: string;
deactivated?: boolean;
location: AvailableScreens;
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
@ -57,6 +60,7 @@ const edges: Edge[] = ['bottom'];
export default function Archived({
testID,
deactivated,
location,
}: Props) {
const theme = useTheme();
const style = getStyleSheet(theme);
@ -94,7 +98,7 @@ export default function Archived({
{...message}
style={style.archivedText}
baseTextStyle={style.baseTextStyle}
location=''
location={location}
/>
<Button
buttonStyle={style.closeButton}

View file

@ -15,6 +15,8 @@ import Archived from './archived';
import DraftHandler from './draft_handler';
import ReadOnly from './read_only';
import type {AvailableScreens} from '@typings/screens/navigation';
const AUTOCOMPLETE_ADJUST = -5;
type Props = {
testID?: string;
@ -30,6 +32,7 @@ type Props = {
containerHeight: number;
isChannelScreen: boolean;
canShowPostPriority?: boolean;
location: AvailableScreens;
}
function PostDraft({
@ -46,6 +49,7 @@ function PostDraft({
containerHeight,
isChannelScreen,
canShowPostPriority,
location,
}: Props) {
const [value, setValue] = useState(message);
const [cursorPosition, setCursorPosition] = useState(message.length);
@ -73,6 +77,7 @@ function PostDraft({
<Archived
testID={archivedTestID}
deactivated={deactivatedChannel}
location={location}
/>
);
}

View file

@ -22,11 +22,13 @@ import {secureGetFromRecord} from '@utils/types';
import LastUsers from './last_users';
import {postTypeMessages} from './messages';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
canDelete: boolean;
currentUserId?: string;
currentUsername?: string;
location: string;
location: AvailableScreens;
post: Post | null;
showJoinLeave: boolean;
testID?: string;

View file

@ -14,10 +14,12 @@ import {secureGetFromRecord} from '@utils/types';
import {postTypeMessages, systemMessages} from './messages';
import type {AvailableScreens} from '@typings/screens/navigation';
type LastUsersProps = {
actor: string;
channelId?: string;
location: string;
location: AvailableScreens;
postType: string;
usernames: string[];
theme: Theme;

View file

@ -4,26 +4,27 @@
import {Image} from 'expo-image';
import React, {type ReactNode} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, Platform, StyleSheet, TouchableOpacity, View} from 'react-native';
import {Platform, StyleSheet, TouchableOpacity, View} from 'react-native';
import {buildAbsoluteUrl} from '@actions/remote/file';
import CompassIcon from '@components/compass_icon';
import ProfilePicture from '@components/profile_picture';
import {Screens, View as ViewConstant} from '@constants';
import {View as ViewConstant} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {openAsBottomSheet} from '@screens/navigation';
import {openUserProfileModal} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
import {ensureString} from '@utils/types';
import type PostModel from '@typings/database/models/servers/post';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
type AvatarProps = {
author?: UserModel;
enablePostIconOverride?: boolean;
isAutoReponse: boolean;
location: string;
location: AvailableScreens;
post: PostModel;
}
@ -91,20 +92,13 @@ const Avatar = ({author, enablePostIconOverride, isAutoReponse, location, post}:
if (!author) {
return;
}
const screen = Screens.USER_PROFILE;
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const closeButtonId = 'close-user-profile';
const props = {
closeButtonId,
openUserProfileModal(intl, theme, {
location,
userId: author.id,
channelId: post.channelId,
location,
userIconOverride: propsIconUrl,
usernameOverride: propsUsername,
};
Keyboard.dismiss();
openAsBottomSheet({screen, title, theme, closeButtonId, props});
});
});
let component = (

View file

@ -23,12 +23,13 @@ import {USER_ROW_HEIGHT} from './users_list/user_list_item';
import type PostModel from '@typings/database/models/servers/post';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
currentUserId: UserModel['id'];
currentUserTimezone: UserModel['timezone'];
hasReactions: boolean;
location: string;
location: AvailableScreens;
post: PostModel;
theme: Theme;
};

View file

@ -3,17 +3,17 @@
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard} from 'react-native';
import FormattedRelativeTime from '@components/formatted_relative_time';
import UserItem from '@components/user_item';
import {Screens} from '@constants';
import {useTheme} from '@context/theme';
import {dismissBottomSheet, openAsBottomSheet} from '@screens/navigation';
import {openUserProfileModal as openUserProfileBottomSheet} from '@screens/navigation';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
export const USER_ROW_HEIGHT = 60;
@ -34,7 +34,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
type Props = {
channelId: string;
location: string;
location: AvailableScreens;
user: UserModel;
userAcknowledgement: number;
timezone?: UserTimezone;
@ -53,14 +53,11 @@ const UserListItem = ({
const handleUserPress = useCallback(async (userProfile: UserProfile) => {
if (userProfile) {
await dismissBottomSheet(Screens.BOTTOM_SHEET);
const screen = Screens.USER_PROFILE;
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const closeButtonId = 'close-user-profile';
const props = {closeButtonId, location, userId: userProfile.id, channelId};
Keyboard.dismiss();
openAsBottomSheet({screen, title, theme, closeButtonId, props});
await openUserProfileBottomSheet(intl, theme, {
userId: userProfile.id,
channelId,
location,
}, Screens.BOTTOM_SHEET);
}
}, [channelId, intl, location, theme]);

View file

@ -10,11 +10,12 @@ import {useIsTablet} from '@hooks/device';
import UserListItem from './user_list_item';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
import type {ListRenderItemInfo} from 'react-native';
type Props = {
channelId: string;
location: string;
location: AvailableScreens;
users: UserModel[];
userAcknowledgements: Record<string, number>;
timezone?: UserTimezone;

View file

@ -17,11 +17,12 @@ import {isStringArray} from '@utils/types';
import type PostModel from '@typings/database/models/servers/post';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
type AddMembersProps = {
channelType: string | null;
currentUser?: UserModel;
location: string;
location: AvailableScreens;
post: PostModel;
theme: Theme;
}

View file

@ -11,9 +11,11 @@ import {useShowMoreAnimatedStyle} from '@hooks/show_more';
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
import {makeStyleSheetFromTheme} from '@utils/theme';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
channelId: string;
location: string;
location: AvailableScreens;
theme: Theme;
value: string;
}

View file

@ -7,9 +7,11 @@ import {View} from 'react-native';
import Markdown from '@components/markdown';
import {makeStyleSheetFromTheme} from '@utils/theme';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
channelId: string;
location: string;
location: AvailableScreens;
theme: Theme;
value: string;
}

View file

@ -13,10 +13,11 @@ import EmbedTitle from './embed_title';
import EmbedSubBindings from './embedded_sub_bindings';
import type PostModel from '@typings/database/models/servers/post';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
embed: AppBinding;
location: string;
location: AvailableScreens;
post: PostModel;
theme: Theme;
}
@ -72,6 +73,7 @@ const EmbeddedBinding = ({embed, location, post, theme}: Props) => {
bindings={cleanedBindings}
post={post}
theme={theme}
location={location}
/>
}
</View>

View file

@ -7,14 +7,21 @@ import ButtonBinding from './button_binding';
import BindingMenu from './menu_binding';
import type PostModel from '@typings/database/models/servers/post';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
bindings: AppBinding[];
post: PostModel;
theme: Theme;
location: AvailableScreens;
}
const EmbeddedSubBindings = ({bindings, post, theme}: Props) => {
const EmbeddedSubBindings = ({
bindings,
post,
theme,
location,
}: Props) => {
const content: React.ReactNode[] = [];
bindings.forEach((binding) => {
@ -28,6 +35,7 @@ const EmbeddedSubBindings = ({bindings, post, theme}: Props) => {
key={binding.location}
binding={binding}
post={post}
location={location}
/>,
);
return;

View file

@ -10,9 +10,10 @@ import {isArrayOf} from '@utils/types';
import EmbeddedBinding from './embedded_binding';
import type PostModel from '@typings/database/models/servers/post';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
location: string;
location: AvailableScreens;
post: PostModel;
theme: Theme;
}

View file

@ -15,15 +15,23 @@ import {logDebug} from '@utils/log';
import type {WithDatabaseArgs} from '@typings/database/database';
import type PostModel from '@typings/database/models/servers/post';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
binding: AppBinding;
currentTeamId: string;
post: PostModel;
teamID?: string;
location: AvailableScreens;
}
const MenuBinding = ({binding, currentTeamId, post, teamID}: Props) => {
const MenuBinding = ({
binding,
currentTeamId,
post,
teamID,
location,
}: Props) => {
const [selected, setSelected] = useState<string>();
const serverUrl = useServerUrl();
@ -73,6 +81,7 @@ const MenuBinding = ({binding, currentTeamId, post, teamID}: Props) => {
selected={selected}
onSelected={onSelect}
testID={`embedded_binding.${binding.location}`}
location={location}
/>
);
};

View file

@ -13,11 +13,12 @@ import Opengraph from './opengraph';
import YouTube from './youtube';
import type PostModel from '@typings/database/models/servers/post';
import type {AvailableScreens} from '@typings/screens/navigation';
type ContentProps = {
isReplyPost: boolean;
layoutWidth?: number;
location: string;
location: AvailableScreens;
post: PostModel;
theme: Theme;
}

View file

@ -8,6 +8,8 @@ import AutocompleteSelector from '@components/autocomplete_selector';
import {useServerUrl} from '@context/server';
import {filterOptions} from '@utils/message_attachment';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
dataSource?: string;
defaultOption?: string;
@ -16,9 +18,19 @@ type Props = {
name: string;
options?: PostActionOption[];
postId: string;
location: AvailableScreens;
}
const ActionMenu = ({dataSource, defaultOption, disabled, id, name, options, postId}: Props) => {
const ActionMenu = ({
dataSource,
defaultOption,
disabled,
id,
name,
options,
postId,
location,
}: Props) => {
const serverUrl = useServerUrl();
const filteredOptions = useMemo(() => {
@ -53,6 +65,7 @@ const ActionMenu = ({dataSource, defaultOption, disabled, id, name, options, pos
onSelected={handleSelect}
disabled={disabled}
testID={`message_attachment.${name}`}
location={location}
/>
);
};

View file

@ -6,12 +6,20 @@ import React from 'react';
import ActionButton from './action_button';
import ActionMenu from './action_menu';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
actions: PostAction[];
postId: string;
theme: Theme;
location: AvailableScreens;
}
const AttachmentActions = ({actions, postId, theme}: Props) => {
const AttachmentActions = ({
actions,
postId,
theme,
location,
}: Props) => {
const content: React.ReactNode[] = [];
actions.forEach((action) => {
@ -31,6 +39,7 @@ const AttachmentActions = ({actions, postId, theme}: Props) => {
options={action.options}
postId={postId}
disabled={action.disabled}
location={location}
/>,
);
break;

View file

@ -8,13 +8,14 @@ import Markdown from '@components/markdown';
import {makeStyleSheetFromTheme} from '@utils/theme';
import type {MarkdownBlockStyles, MarkdownTextStyles} from '@typings/global/markdown';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
baseTextStyle: StyleProp<TextStyle>;
blockStyles?: MarkdownBlockStyles;
channelId: string;
fields: MessageAttachmentField[];
location: string;
location: AvailableScreens;
metadata?: PostMetadata | null;
textStyles?: MarkdownTextStyles;
theme: Theme;

View file

@ -7,12 +7,13 @@ import {type StyleProp, StyleSheet, type TextStyle, View} from 'react-native';
import Markdown from '@components/markdown';
import type {MarkdownBlockStyles, MarkdownTextStyles} from '@typings/global/markdown';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
baseTextStyle: StyleProp<TextStyle>;
blockStyles?: MarkdownBlockStyles;
channelId: string;
location: string;
location: AvailableScreens;
metadata?: PostMetadata | null;
textStyles?: MarkdownTextStyles;
theme: Theme;

View file

@ -10,13 +10,14 @@ import ShowMoreButton from '@components/post_list/post/body/message/show_more_bu
import {useShowMoreAnimatedStyle} from '@hooks/show_more';
import type {MarkdownBlockStyles, MarkdownTextStyles} from '@typings/global/markdown';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
baseTextStyle: StyleProp<TextStyle>;
blockStyles?: MarkdownBlockStyles;
channelId: string;
hasThumbnail?: boolean;
location: string;
location: AvailableScreens;
metadata?: PostMetadata | null;
textStyles?: MarkdownTextStyles;
theme: Theme;

View file

@ -9,10 +9,12 @@ import Markdown from '@components/markdown';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {tryOpenURL} from '@utils/url';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
channelId: string;
link?: string;
location: string;
location: AvailableScreens;
theme: Theme;
value?: string;
}

View file

@ -6,11 +6,13 @@ import {StyleSheet, View} from 'react-native';
import MessageAttachment from './message_attachment';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
attachments: MessageAttachment[];
channelId: string;
layoutWidth?: number;
location: string;
location: AvailableScreens;
metadata?: PostMetadata | undefined | null;
postId: string;
theme: Theme;

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import {render} from '@testing-library/react-native';
import React from 'react';
import React, {type ComponentProps} from 'react';
import Preferences from '@constants/preferences';
@ -81,7 +81,7 @@ describe('MessageAttachment', () => {
jest.clearAllMocks();
});
const baseProps = {
const baseProps: ComponentProps<typeof MessageAttachment> = {
attachment: {
id: 1,
text: 'This is the text of an attachment',
@ -113,7 +113,7 @@ describe('MessageAttachment', () => {
},
channelId: 'channel-id',
postId: 'post-id',
location: 'CENTER',
location: 'Channel',
theme: Preferences.THEMES.denim,
metadata: {
images: {

View file

@ -20,11 +20,13 @@ import AttachmentText from './attachment_text';
import AttachmentThumbnail from './attachment_thumbnail';
import AttachmentTitle from './attachment_title';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
attachment: MessageAttachment;
channelId: string;
layoutWidth?: number;
location: string;
location: AvailableScreens;
metadata?: PostMetadata | null;
postId: string;
theme: Theme;
@ -138,6 +140,7 @@ export default function MessageAttachment({attachment, channelId, layoutWidth, l
actions={attachment.actions!}
postId={postId}
theme={theme}
location={location}
/>
}
{attachment.image_url && Boolean(metadata?.images?.[attachment.image_url]) &&

View file

@ -21,6 +21,7 @@ import Reactions from './reactions';
import type PostModel from '@typings/database/models/servers/post';
import type {SearchPattern} from '@typings/global/markdown';
import type {AvailableScreens} from '@typings/screens/navigation';
type BodyProps = {
appsEnabled: boolean;
@ -36,7 +37,7 @@ type BodyProps = {
isPendingOrFailed: boolean;
isPostAcknowledgementEnabled?: boolean;
isPostAddChannelMember: boolean;
location: string;
location: AvailableScreens;
post: PostModel;
searchPatterns?: SearchPattern[];
showAddReaction?: boolean;

View file

@ -18,6 +18,7 @@ import ShowMoreButton from './show_more_button';
import type PostModel from '@typings/database/models/servers/post';
import type UserModel from '@typings/database/models/servers/user';
import type {HighlightWithoutNotificationKey, SearchPattern, UserMentionKey} from '@typings/global/markdown';
import type {AvailableScreens} from '@typings/screens/navigation';
type MessageProps = {
currentUser?: UserModel;
@ -27,7 +28,7 @@ type MessageProps = {
isPendingOrFailed: boolean;
isReplyPost: boolean;
layoutWidth?: number;
location: string;
location: AvailableScreens;
post: PostModel;
searchPatterns?: SearchPattern[];
theme: Theme;

View file

@ -16,10 +16,11 @@ import {typography} from '@utils/typography';
import type ThreadModel from '@typings/database/models/servers/thread';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
channelId: string;
location: string;
location: AvailableScreens;
participants: UserModel[];
teamId?: string;
thread: ThreadModel;

View file

@ -3,21 +3,22 @@
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, Text, TouchableOpacity, useWindowDimensions, View} from 'react-native';
import {Text, TouchableOpacity, useWindowDimensions, View} from 'react-native';
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
import FormattedText from '@components/formatted_text';
import {Screens} from '@constants';
import {openAsBottomSheet} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
import {usePreventDoubleTap} from '@hooks/utils';
import {openUserProfileModal} from '@screens/navigation';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import type {AvailableScreens} from '@typings/screens/navigation';
type HeaderDisplayNameProps = {
channelId: string;
commentCount: number;
displayName?: string;
location: string;
location: AvailableScreens;
rootPostAuthor?: string;
shouldRenderReplyButton?: boolean;
theme: Theme;
@ -71,15 +72,15 @@ const HeaderDisplayName = ({
const intl = useIntl();
const style = getStyleSheet(theme);
const onPress = useCallback(preventDoubleTap(() => {
const screen = Screens.USER_PROFILE;
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const closeButtonId = 'close-user-profile';
const props = {closeButtonId, userId, channelId, location, userIconOverride, usernameOverride};
Keyboard.dismiss();
openAsBottomSheet({screen, title, theme, closeButtonId, props});
}), [intl.locale, channelId, userIconOverride, userId, usernameOverride, theme]);
const onPress = usePreventDoubleTap(useCallback(() => {
openUserProfileModal(intl, theme, {
location,
userId,
channelId,
userIconOverride,
usernameOverride,
});
}, [intl, userId, channelId, location, userIconOverride, usernameOverride, theme]));
const calcNameWidth = () => {
const isLandscape = dimensions.width > dimensions.height;

View file

@ -23,6 +23,7 @@ import HeaderTag from './tag';
import type PostModel from '@typings/database/models/servers/post';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
type HeaderProps = {
author?: UserModel;
@ -37,7 +38,7 @@ type HeaderProps = {
isPendingOrFailed: boolean;
isSystemPost: boolean;
isWebHook: boolean;
location: string;
location: AvailableScreens;
post: PostModel;
rootPostAuthor?: UserModel;
showPostPriority: boolean;

View file

@ -36,6 +36,7 @@ import type PostModel from '@typings/database/models/servers/post';
import type ThreadModel from '@typings/database/models/servers/thread';
import type UserModel from '@typings/database/models/servers/user';
import type {SearchPattern} from '@typings/global/markdown';
import type {AvailableScreens} from '@typings/screens/navigation';
type PostProps = {
appsEnabled: boolean;
@ -57,7 +58,7 @@ type PostProps = {
isLastReply?: boolean;
isPostAddChannelMember: boolean;
isPostPriorityEnabled: boolean;
location: string;
location: AvailableScreens;
post: PostModel;
rootId?: string;
previousPost?: PostModel;

View file

@ -17,11 +17,12 @@ import {typography} from '@utils/typography';
import type PostModel from '@typings/database/models/servers/post';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
import type {PrimitiveType} from 'intl-messageformat';
type SystemMessageProps = {
author?: UserModel;
location: string;
location: AvailableScreens;
post: PostModel;
}

View file

@ -25,6 +25,7 @@ import ScrollToEndView from './scroll_to_end_view';
import type {PostListItem, PostListOtherItem, ViewableItemsChanged, ViewableItemsChangedListenerEvent} from '@typings/components/post_list';
import type PostModel from '@typings/database/models/servers/post';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
appsEnabled: boolean;
@ -40,7 +41,7 @@ type Props = {
isCRTEnabled?: boolean;
isPostAcknowledgementEnabled?: boolean;
lastViewedAt: number;
location: string;
location: AvailableScreens;
nativeID: string;
onEndReached?: () => void;
posts: PostModel[];

View file

@ -10,13 +10,14 @@ import ChannelInfo from './channel_info';
import type PostModel from '@typings/database/models/servers/post';
import type {SearchPattern} from '@typings/global/markdown';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
appsEnabled: boolean;
customEmojiNames: string[];
isCRTEnabled: boolean;
post: PostModel;
location: string;
location: AvailableScreens;
testID?: string;
searchPatterns?: SearchPattern[];
skipSavedPostsHighlight?: boolean;

View file

@ -3,6 +3,7 @@
import React, {type ComponentProps} from 'react';
import {Screens} from '@constants';
import {fireEvent, renderWithEverything} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
@ -38,6 +39,7 @@ function getBaseProps(): ComponentProps<typeof SectionNotice> {
loading: true,
},
onDismissClick: jest.fn(),
location: Screens.SETTINGS_NOTIFICATION_PUSH,
};
}

View file

@ -13,6 +13,8 @@ import Markdown from '../markdown';
import SectionNoticeButton from './section_notice_button';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
title: string;
text?: string;
@ -22,6 +24,7 @@ type Props = {
type?: 'info' | 'success' | 'danger' | 'welcome' | 'warning' | 'hint';
isDismissable?: boolean;
onDismissClick?: () => void;
location: AvailableScreens;
}
const iconByType = {
@ -150,6 +153,7 @@ const SectionNotice = ({
secondaryButton,
text,
type = 'info',
location,
}: Props) => {
const theme = useTheme();
const styles = getStyleFromTheme(theme);
@ -179,7 +183,7 @@ const SectionNotice = ({
{text && (
<Markdown
theme={theme}
location=''
location={location}
baseTextStyle={styles.baseText}
value={text}
/>

View file

@ -9,6 +9,8 @@ import {useServerUrl} from '@context/server';
import {debounce} from '@helpers/api/general';
import {filterProfilesMatchingTerm} from '@utils/user';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
currentUserId: string;
tutorialWatched: boolean;
@ -19,6 +21,7 @@ type Props = {
searchFunction: (term: string) => Promise<UserProfile[]>;
createFilter: (exactMatches: UserProfile[], term: string) => ((p: UserProfile) => boolean);
testID: string;
location: AvailableScreens;
}
export default function ServerUserList({
@ -31,6 +34,7 @@ export default function ServerUserList({
searchFunction,
createFilter,
testID,
location,
}: Props) {
const serverUrl = useServerUrl();
@ -124,6 +128,7 @@ export default function ServerUserList({
testID={testID}
tutorialWatched={tutorialWatched}
includeUserMargin={true}
location={location}
/>
);
}

View file

@ -10,6 +10,8 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import Footer from './footer';
import Label from './label';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
label?: string;
value?: boolean;
@ -21,6 +23,7 @@ type Props = {
disabled?: boolean;
onChange: (value: boolean) => void;
testID: string;
location: AvailableScreens;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
@ -62,6 +65,7 @@ function BoolSetting({
disabled = false,
onChange,
testID,
location,
}: Props) {
const theme = useTheme();
const style = getStyleSheet(theme);
@ -98,6 +102,7 @@ function BoolSetting({
disabledText={disabledText}
errorText={errorText}
helpText={helpText}
location={location}
/>
</View>
</>

View file

@ -9,6 +9,8 @@ import {useTheme} from '@context/theme';
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import type {AvailableScreens} from '@typings/screens/navigation';
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
helpTextContainer: {
@ -35,12 +37,14 @@ type Props = {
disabledText?: string;
helpText?: string;
errorText?: string;
location: AvailableScreens;
}
function Footer({
disabled,
disabledText,
helpText,
errorText,
location,
}: Props) {
const theme = useTheme();
const style = getStyleSheet(theme);
@ -55,7 +59,7 @@ function Footer({
baseTextStyle={style.helpText}
textStyles={textStyles}
disableAtMentions={true}
location=''
location={location}
blockStyles={blockStyles}
value={disabledText}
theme={theme}
@ -69,7 +73,7 @@ function Footer({
textStyles={textStyles}
blockStyles={blockStyles}
disableAtMentions={true}
location=''
location={location}
value={helpText}
theme={theme}
/>
@ -82,7 +86,7 @@ function Footer({
textStyles={textStyles}
blockStyles={blockStyles}
disableAtMentions={true}
location=''
location={location}
value={errorText}
theme={theme}
/>

View file

@ -12,6 +12,8 @@ import Label from '../label';
import RadioEntry from './radio_entry';
import type {AvailableScreens} from '@typings/screens/navigation';
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
items: {
@ -32,6 +34,7 @@ type Props = {
errorText?: string;
value?: string;
testID: string;
location: AvailableScreens;
}
function RadioSetting({
label,
@ -41,6 +44,7 @@ function RadioSetting({
errorText = '',
testID,
value,
location,
}: Props) {
const theme = useTheme();
const style = getStyleSheet(theme);
@ -80,6 +84,7 @@ function RadioSetting({
disabled={false}
errorText={errorText}
helpText={helpText}
location={location}
/>
</View>
);

View file

@ -14,6 +14,8 @@ import {
import Footer from './footer';
import Label from './label';
import type {AvailableScreens} from '@typings/screens/navigation';
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
const input = {
color: theme.centerChannelColor,
@ -60,6 +62,7 @@ type Props = {
keyboardType: KeyboardTypeOptions;
secureTextEntry: boolean;
testID: string;
location: AvailableScreens;
}
function TextSetting({
label,
@ -76,6 +79,7 @@ function TextSetting({
keyboardType,
secureTextEntry,
testID,
location,
}: Props) {
const theme = useTheme();
const style = getStyleSheet(theme);
@ -123,6 +127,7 @@ function TextSetting({
disabledText={disabledText}
errorText={errorText}
helpText={helpText}
location={location}
/>
</View>
</View>

View file

@ -20,13 +20,14 @@ import UserAvatar from './user_avatar';
import UsersList from './users_list';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
const OVERFLOW_DISPLAY_LIMIT = 99;
const USER_ROW_HEIGHT = 40;
type Props = {
channelId?: string;
location: string;
location: AvailableScreens;
users: UserModel[];
breakAt?: number;
style?: StyleProp<ViewStyle>;

View file

@ -4,27 +4,28 @@
import {BottomSheetFlatList} from '@gorhom/bottom-sheet';
import React, {useCallback, useRef} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, type ListRenderItemInfo, type NativeScrollEvent, type NativeSyntheticEvent} from 'react-native';
import {type ListRenderItemInfo, type NativeScrollEvent, type NativeSyntheticEvent} from 'react-native';
import {FlatList} from 'react-native-gesture-handler';
import UserItem from '@components/user_item';
import {Screens} from '@constants';
import {useTheme} from '@context/theme';
import {useBottomSheetListsFix} from '@hooks/bottom_sheet_lists_fix';
import {dismissBottomSheet, openAsBottomSheet} from '@screens/navigation';
import {openUserProfileModal} from '@screens/navigation';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
channelId?: string;
location: string;
location: AvailableScreens;
type?: BottomSheetList;
users: UserModel[];
};
type ItemProps = {
channelId?: string;
location: string;
location: AvailableScreens;
user: UserModel;
}
@ -33,14 +34,11 @@ const Item = ({channelId, location, user}: ItemProps) => {
const theme = useTheme();
const openUserProfile = useCallback(async (u: UserModel | UserProfile) => {
await dismissBottomSheet(Screens.BOTTOM_SHEET);
const screen = Screens.USER_PROFILE;
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const closeButtonId = 'close-user-profile';
const props = {closeButtonId, location, userId: u.id, channelId};
Keyboard.dismiss();
openAsBottomSheet({screen, title, theme, closeButtonId, props});
openUserProfileModal(intl, theme, {
userId: u.id,
channelId,
location,
}, Screens.BOTTOM_SHEET);
}, [location, channelId, theme, intl]);
return (

View file

@ -629,7 +629,7 @@ exports[`components/channel_list_row should show results and tutorial 1`] = `
keyboardShouldPersistTaps="always"
maxToRenderPerBatch={16}
onContentSizeChange={[Function]}
onEndReached={[Function]}
onEndReached={[MockFunction]}
onLayout={[Function]}
onMomentumScrollBegin={[Function]}
onMomentumScrollEnd={[Function]}
@ -949,7 +949,7 @@ exports[`components/channel_list_row should show results no tutorial 1`] = `
keyboardShouldPersistTaps="always"
maxToRenderPerBatch={16}
onContentSizeChange={[Function]}
onEndReached={[Function]}
onEndReached={[MockFunction]}
onLayout={[Function]}
onMomentumScrollBegin={[Function]}
onMomentumScrollEnd={[Function]}
@ -1307,7 +1307,7 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`]
keyboardShouldPersistTaps="always"
maxToRenderPerBatch={16}
onContentSizeChange={[Function]}
onEndReached={[Function]}
onEndReached={[MockFunction]}
onLayout={[Function]}
onMomentumScrollBegin={[Function]}
onMomentumScrollEnd={[Function]}

View file

@ -1,9 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import React, {type ComponentProps} from 'react';
import {Image} from 'react-native';
import {Screens} from '@constants';
import {Ringtone} from '@constants/calls';
import {renderWithEverything} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
@ -108,24 +109,26 @@ describe('components/channel_list_row', () => {
Image.resolveAssetSource = originalResolveAssetSource;
});
function getBaseProps(): ComponentProps<typeof UserList> {
return {
profiles: [],
testID: 'UserListRow',
currentUserId: '1',
handleSelectProfile: jest.fn(),
fetchMore: jest.fn(),
loading: true,
selectedIds: {},
showNoResults: true,
tutorialWatched: true,
term: 'some term',
location: Screens.CHANNEL,
};
}
it('should show no results', () => {
const props = getBaseProps();
const wrapper = renderWithEverything(
<UserList
profiles={[]}
testID='UserListRow'
currentUserId={'1'}
handleSelectProfile={() => {
// noop
}}
fetchMore={() => {
// noop
}}
loading={true}
selectedIds={{}}
term={'some term'}
showNoResults={true}
tutorialWatched={true}
/>,
<UserList {...props}/>,
{database},
);
@ -133,22 +136,12 @@ describe('components/channel_list_row', () => {
});
it('should show results no tutorial', () => {
const props = getBaseProps();
props.profiles = [user];
props.term = '';
const wrapper = renderWithEverything(
<UserList
profiles={[user]}
testID='UserListRow'
currentUserId={'1'}
handleSelectProfile={() => {
// noop
}}
fetchMore={() => {
// noop
}}
loading={true}
selectedIds={{}}
showNoResults={true}
tutorialWatched={true}
/>,
<UserList {...props}/>,
{database},
);
@ -156,22 +149,11 @@ describe('components/channel_list_row', () => {
});
it('should show results no tutorial 2 users', () => {
const props = getBaseProps();
props.profiles = [user, user2];
props.term = '';
const wrapper = renderWithEverything(
<UserList
profiles={[user, user2]}
testID='UserListRow'
currentUserId={'1'}
handleSelectProfile={() => {
// noop
}}
fetchMore={() => {
// noop
}}
loading={true}
selectedIds={{}}
showNoResults={true}
tutorialWatched={true}
/>,
<UserList {...props}/>,
{database},
);
@ -179,22 +161,13 @@ describe('components/channel_list_row', () => {
});
it('should show results and tutorial', () => {
const props = getBaseProps();
props.profiles = [user];
props.showNoResults = false;
props.tutorialWatched = false;
props.term = '';
const wrapper = renderWithEverything(
<UserList
profiles={[user]}
testID='UserListRow'
currentUserId={'1'}
handleSelectProfile={() => {
// noop
}}
fetchMore={() => {
// noop
}}
loading={true}
selectedIds={{}}
showNoResults={false}
tutorialWatched={false}
/>,
<UserList {...props}/>,
{database},
);

View file

@ -9,11 +9,11 @@ import {storeProfile} from '@actions/local/user';
import Loading from '@components/loading';
import NoResultsWithTerm from '@components/no_results_with_term';
import UserListRow from '@components/user_list_row';
import {General, Screens} from '@constants';
import {General} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useKeyboardHeight} from '@hooks/device';
import {openAsBottomSheet} from '@screens/navigation';
import {openUserProfileModal} from '@screens/navigation';
import {
changeOpacity,
makeStyleSheetFromTheme,
@ -21,6 +21,7 @@ import {
import {typography} from '@utils/typography';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
type UserProfileWithChannelAdmin = UserProfile & {scheme_admin?: boolean}
type RenderItemType = ListRenderItemInfo<UserProfileWithChannelAdmin> & {section?: SectionListData<UserProfileWithChannelAdmin>}
@ -183,6 +184,7 @@ type Props = {
term?: string;
tutorialWatched: boolean;
includeUserMargin?: boolean;
location: AvailableScreens;
}
export default function UserList({
@ -200,6 +202,7 @@ export default function UserList({
testID,
tutorialWatched,
includeUserMargin,
location,
}: Props) {
const intl = useIntl();
const theme = useTheme();
@ -235,18 +238,11 @@ export default function UserList({
user = profile;
}
const screen = Screens.USER_PROFILE;
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const closeButtonId = 'close-user-profile';
const props = {
closeButtonId,
openUserProfileModal(intl, theme, {
userId: user.id,
location: Screens.USER_PROFILE,
};
Keyboard.dismiss();
openAsBottomSheet({screen, title, theme, closeButtonId, props});
}, [intl, serverUrl, theme]);
location,
});
}, [intl, location, serverUrl, theme]);
const renderItem = useCallback(({item, index, section}: RenderItemType) => {
// The list will re-render when the selection changes because it's passed into the list as extraData

View file

@ -119,6 +119,7 @@ jest.mock('react-native-navigation', () => ({
pop: jest.fn(() => Promise.resolve({
catch: jest.fn(),
})),
setDefaultOptions: jest.fn(),
},
}));

View file

@ -25,7 +25,7 @@ import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager';
import {queryAllActiveServers} from '@queries/app/servers';
import {getCurrentUser} from '@queries/servers/user';
import {openAsBottomSheet} from '@screens/navigation';
import {openUserProfileModal, openAsBottomSheet} from '@screens/navigation';
import {useTryCallsFunction, usePermissionsChecker, useCallsAdjustment, useHostControlsAvailable, useHostMenus} from './hooks';
@ -74,6 +74,7 @@ jest.mock('@queries/servers/user', () => ({
jest.mock('@screens/navigation', () => ({
openAsBottomSheet: jest.fn(),
openUserProfileModal: jest.fn(),
}));
describe('Calls Hooks', () => {
@ -267,9 +268,13 @@ describe('Calls Hooks', () => {
await result.current.onPress(mockSession)();
});
expect(openAsBottomSheet).toHaveBeenCalledWith(expect.objectContaining({
screen: Screens.USER_PROFILE,
}));
expect(openUserProfileModal).toHaveBeenCalledWith(
expect.any(Object),
expect.any(Object),
expect.objectContaining({
userId: mockSession.userId,
}),
);
});
});
});

View file

@ -35,7 +35,7 @@ import {useAppState} from '@hooks/device';
import NetworkManager from '@managers/network_manager';
import {queryAllActiveServers} from '@queries/app/servers';
import {getCurrentUser} from '@queries/servers/user';
import {openAsBottomSheet} from '@screens/navigation';
import {openAsBottomSheet, openUserProfileModal} from '@screens/navigation';
import {getFullErrorMessage} from '@utils/errors';
import {isSystemAdmin} from '@utils/user';
@ -211,12 +211,9 @@ export const useHostMenus = () => {
}, [intl, theme]);
const openUserProfile = useCallback(async (session: CallSession) => {
const screen = Screens.USER_PROFILE;
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const closeUserProfile = 'close-user-profile';
const props = {closeButtonId: closeUserProfile, location: '', userId: session.userId};
openAsBottomSheet({screen, title, theme, closeButtonId: closeUserProfile, props});
openUserProfileModal(intl, theme, {
userId: session.userId,
});
}, [intl, theme]);
const onPress = useCallback((session: CallSession) => () => {

View file

@ -113,6 +113,7 @@ jest.mock('react-native-navigation', () => ({
pop: jest.fn(() => Promise.resolve({
catch: jest.fn(),
})),
setDefaultOptions: jest.fn(),
},
}));

View file

@ -10,6 +10,7 @@ import {SafeAreaView} from 'react-native-safe-area-context';
import {handleGotoLocation} from '@actions/remote/command';
import CompassIcon from '@components/compass_icon';
import Markdown from '@components/markdown';
import {Screens} from '@constants';
import {AppCallResponseTypes} from '@constants/apps';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
@ -401,7 +402,7 @@ function AppsFormComponent({
baseTextStyle={style.errorLabel}
textStyles={getMarkdownTextStyles(theme)}
blockStyles={getMarkdownBlockStyles(theme)}
location=''
location={Screens.APPS_FORM}
disableAtMentions={true}
value={error}
theme={theme}

View file

@ -8,7 +8,7 @@ import AutocompleteSelector from '@components/autocomplete_selector';
import Markdown from '@components/markdown';
import BoolSetting from '@components/settings/bool_setting';
import TextSetting from '@components/settings/text_setting';
import {View as ViewConstants} from '@constants';
import {Screens, View as ViewConstants} from '@constants';
import {AppFieldTypes, SelectableAppFieldTypes} from '@constants/apps';
import {useTheme} from '@context/theme';
import {selectKeyboardType} from '@utils/integrations';
@ -161,6 +161,7 @@ function AppsFormField({
secureTextEntry={field.subtype === 'password'}
disabled={Boolean(field.readonly)}
testID={testID}
location={Screens.APPS_FORM}
/>
);
}
@ -185,6 +186,7 @@ function AppsFormField({
disabled={field.readonly}
isMultiselect={field.multiselect}
testID={testID}
location={Screens.APPS_FORM}
/>
);
}
@ -200,6 +202,7 @@ function AppsFormField({
onChange={handleChange}
disabled={field.readonly}
testID={testID}
location={Screens.APPS_FORM}
/>
);
}
@ -216,7 +219,7 @@ function AppsFormField({
value={field.description}
mentionKeys={[]}
disableAtMentions={true}
location=''
location={Screens.APPS_FORM}
blockStyles={getMarkdownBlockStyles(theme)}
textStyles={getMarkdownTextStyles(theme)}
baseTextStyle={style.markdownFieldText}

View file

@ -10,6 +10,7 @@ import FloatingCallContainer from '@calls/components/floating_call_container';
import FreezeScreen from '@components/freeze_screen';
import PostDraft from '@components/post_draft';
import ScheduledPostIndicator from '@components/scheduled_post_indicator';
import {Screens} from '@constants';
import {ExtraKeyboardProvider} from '@context/extra_keyboard';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useChannelSwitch} from '@hooks/channel_switch';
@ -152,6 +153,7 @@ const Channel = ({
containerHeight={containerHeight}
isChannelScreen={true}
canShowPostPriority={true}
location={Screens.CHANNEL}
/>
</ExtraKeyboardProvider>
}

View file

@ -3,12 +3,12 @@
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, type StyleProp, StyleSheet, type ViewStyle} from 'react-native';
import {type StyleProp, StyleSheet, type ViewStyle} from 'react-native';
import ProfilePicture from '@components/profile_picture';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {Screens} from '@constants';
import {openAsBottomSheet} from '@screens/navigation';
import {openUserProfileModal} from '@screens/navigation';
import type UserModel from '@typings/database/models/servers/user';
@ -32,13 +32,11 @@ const styles = StyleSheet.create({
const Member = ({channelId, containerStyle, size = 72, showStatus = true, theme, user}: Props) => {
const intl = useIntl();
const onPress = useCallback(() => {
const screen = Screens.USER_PROFILE;
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const closeButtonId = 'close-user-profile';
const props = {closeButtonId, userId: user.id, channelId, location: Screens.CHANNEL};
Keyboard.dismiss();
openAsBottomSheet({screen, title, theme, closeButtonId, props});
openUserProfileModal(intl, theme, {
userId: user.id,
channelId,
location: Screens.CHANNEL,
});
}, [intl, user.id, channelId, theme]);
return (

View file

@ -13,7 +13,7 @@ import Loading from '@components/loading';
import Search from '@components/search';
import SelectedUsers from '@components/selected_users';
import ServerUserList from '@components/server_user_list';
import {General} from '@constants';
import {General, Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
@ -281,6 +281,7 @@ export default function ChannelAddMembers({
fetchFunction={userFetchFunction}
searchFunction={userSearchFunction}
createFilter={createUserFilter}
location={Screens.CHANNEL_ADD_MEMBERS}
/>
<SelectedUsers
keyboardOverlap={keyboardOverlap}

View file

@ -6,6 +6,7 @@ import React, {useCallback, useMemo, useState} from 'react';
import AutocompleteSelector from '@components/autocomplete_selector';
import BoolSetting from '@components/settings/bool_setting';
import TextSetting from '@components/settings/text_setting';
import {Screens} from '@constants';
type HookResult<T> = [
{[x: string]: T},
@ -28,6 +29,7 @@ export const useStringProp = (
secureTextEntry={false}
testID={`${propName}.input`}
value={value}
location={Screens.COMPONENT_LIBRARY}
/>
), [value, propName, isTextarea]);
const preparedProp = useMemo(() => ({[propName]: value}), [propName, value]);
@ -46,6 +48,7 @@ export const useBooleanProp = (
testID={`${propName}.input`}
value={value}
label={propName}
location={Screens.COMPONENT_LIBRARY}
/>
), [propName, value]);
const preparedProp = useMemo(() => ({[propName]: value}), [propName, value]);
@ -99,6 +102,7 @@ export const useDropdownProp = (
onSelected={onChange}
options={renderedOptions}
selected={value}
location={Screens.COMPONENT_LIBRARY}
/>
), [onChange, propName, renderedOptions, value]);
const preparedProp = useMemo(() => (value === ALL_OPTION ? undefined : ({[propName]: value})), [propName, value]);

View file

@ -5,7 +5,7 @@ import React, {useCallback, useMemo, useState} from 'react';
import {ScrollView, View, type StyleProp, type ViewStyle} from 'react-native';
import AutocompleteSelector from '@components/autocomplete_selector';
import {Preferences} from '@constants';
import {Preferences, Screens} from '@constants';
import {CustomThemeProvider} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import SecurityManager from '@managers/security_manager';
@ -122,6 +122,7 @@ const ComponentLibrary = ({componentId}: Props) => {
onSelected={onSelectComponent}
selected={selectedComponent}
options={componentOptions}
location={Screens.COMPONENT_LIBRARY}
/>
<AutocompleteSelector
testID='selectedTheme'
@ -129,6 +130,7 @@ const ComponentLibrary = ({componentId}: Props) => {
onSelected={onSelectTheme}
selected={selectedTheme}
options={themeOptions}
location={Screens.COMPONENT_LIBRARY}
/>
<AutocompleteSelector
testID='selectedBackground'
@ -136,6 +138,7 @@ const ComponentLibrary = ({componentId}: Props) => {
onSelected={onSelectBackground}
selected={selectedBackground}
options={backgroundOptions}
location={Screens.COMPONENT_LIBRARY}
/>
<View style={backgroundStyle}>
<CustomThemeProvider theme={Preferences.THEMES[selectedTheme]}>

View file

@ -13,7 +13,7 @@ import Loading from '@components/loading';
import Search from '@components/search';
import SelectedUsers from '@components/selected_users';
import ServerUserList from '@components/server_user_list';
import {General} from '@constants';
import {General, Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
@ -326,6 +326,7 @@ export default function CreateDirectMessage({
fetchFunction={userFetchFunction}
searchFunction={userSearchFunction}
createFilter={createUserFilter}
location={Screens.CREATE_DIRECT_MESSAGE}
/>
<SelectedUsers
keyboardOverlap={keyboardOverlap}

View file

@ -5,7 +5,7 @@ import {fireEvent, waitFor} from '@testing-library/react-native';
import React from 'react';
import {DeviceEventEmitter} from 'react-native';
import {Events} from '@constants';
import {Events, Screens} from '@constants';
import {renderWithIntl} from '@test/intl-test-helper';
import * as DraftUtils from '@utils/draft';
import * as ScheduledPostUtils from '@utils/scheduled_post';
@ -65,7 +65,7 @@ describe('DraftAndScheduledPostSwipeActions', () => {
<DraftAndScheduledPostSwipeActions
draftType={DRAFT_TYPE_DRAFT}
item={mockDraft}
location='global_drafts'
location={Screens.GLOBAL_DRAFTS}
layoutWidth={300}
/>,
);
@ -79,7 +79,7 @@ describe('DraftAndScheduledPostSwipeActions', () => {
<DraftAndScheduledPostSwipeActions
draftType={DRAFT_TYPE_SCHEDULED}
item={mockScheduledPost}
location='global_drafts'
location={Screens.GLOBAL_DRAFTS}
layoutWidth={300}
/>,
);
@ -95,7 +95,7 @@ describe('DraftAndScheduledPostSwipeActions', () => {
<DraftAndScheduledPostSwipeActions
draftType={DRAFT_TYPE_DRAFT}
item={mockDraft}
location='global_drafts'
location={Screens.GLOBAL_DRAFTS}
layoutWidth={300}
/>,
);
@ -110,7 +110,7 @@ describe('DraftAndScheduledPostSwipeActions', () => {
<DraftAndScheduledPostSwipeActions
draftType={DRAFT_TYPE_DRAFT}
item={mockDraft}
location='global_drafts'
location={Screens.GLOBAL_DRAFTS}
layoutWidth={300}
/>,
);
@ -134,7 +134,7 @@ describe('DraftAndScheduledPostSwipeActions', () => {
<DraftAndScheduledPostSwipeActions
draftType={DRAFT_TYPE_SCHEDULED}
item={mockScheduledPost}
location='global_drafts'
location={Screens.GLOBAL_DRAFTS}
layoutWidth={300}
/>,
);

View file

@ -22,11 +22,12 @@ import {typography} from '@utils/typography';
import type DraftModel from '@typings/database/models/servers/draft';
import type ScheduledPostModel from '@typings/database/models/servers/scheduled_post';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
draftType: DraftType;
item: DraftModel | ScheduledPostModel;
location: string;
location: AvailableScreens;
layoutWidth: number;
firstItem?: boolean;
}

View file

@ -17,10 +17,11 @@ import DraftAndScheduledPostSwipeActions from '../draft_and_scheduled_post_swipe
import DraftEmptyComponent from '../draft_empty_component';
import type DraftModel from '@typings/database/models/servers/draft';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
allDrafts: DraftModel[];
location: string;
location: AvailableScreens;
tutorialWatched: boolean;
}

View file

@ -21,10 +21,11 @@ import DraftAndScheduledPostSwipeActions from '../draft_and_scheduled_post_swipe
import ScheduledPostEmptyComponent from '../scheduled_post_empty_component';
import type ScheduledPostModel from '@typings/database/models/servers/scheduled_post';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
allScheduledPosts: ScheduledPostModel[];
location: string;
location: AvailableScreens;
tutorialWatched: boolean;
};

View file

@ -28,11 +28,12 @@ import type ChannelModel from '@typings/database/models/servers/channel';
import type PostModel from '@typings/database/models/servers/post';
import type ThreadModel from '@typings/database/models/servers/thread';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
author?: UserModel;
channel?: ChannelModel;
location: string;
location: AvailableScreens;
post?: PostModel;
teammateNameDisplay: string;
testID: string;

View file

@ -12,11 +12,12 @@ import {typography} from '@utils/typography';
import type ThreadModel from '@typings/database/models/servers/thread';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
author?: UserModel;
channelId: string;
location: string;
location: AvailableScreens;
participants: UserModel[];
testID: string;
thread: ThreadModel;

View file

@ -11,7 +11,7 @@ import {fetchProfiles, searchProfiles} from '@actions/remote/user';
import FormattedText from '@components/formatted_text';
import SearchBar from '@components/search';
import ServerUserList from '@components/server_user_list';
import {General, View as ViewConstants} from '@constants';
import {General, Screens, View as ViewConstants} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {debounce} from '@helpers/api/general';
@ -571,6 +571,7 @@ function IntegrationSelector(
searchFunction={userSearchFunction}
createFilter={createUserFilter}
testID={'integration_selector.user_list'}
location={Screens.INTEGRATION_SELECTOR}
/>
);
default:

View file

@ -7,6 +7,7 @@ import AutocompleteSelector from '@components/autocomplete_selector';
import BoolSetting from '@components/settings/bool_setting';
import RadioSetting from '@components/settings/radio_setting';
import TextSetting from '@components/settings/text_setting';
import {Screens} from '@constants';
import {selectKeyboardType as selectKB} from '@utils/integrations';
import {filterOptions} from '@utils/message_attachment';
@ -109,6 +110,7 @@ function DialogElement({
secureTextEntry={subtype === 'password'}
disabled={false}
testID={testID}
location={Screens.INTERACTIVE_DIALOG}
/>
);
case 'select':
@ -126,6 +128,7 @@ function DialogElement({
selected={getStringValue(value)}
roundedBorders={false}
testID={testID}
location={Screens.INTERACTIVE_DIALOG}
/>
);
case 'radio':
@ -138,6 +141,7 @@ function DialogElement({
onChange={handleChange}
testID={testID}
value={getStringValue(value)}
location={Screens.INTERACTIVE_DIALOG}
/>
);
case 'bool':
@ -151,6 +155,7 @@ function DialogElement({
optional={optional}
onChange={handleChange}
testID={testID}
location={Screens.INTERACTIVE_DIALOG}
/>
);
default:

View file

@ -5,6 +5,7 @@ import React from 'react';
import {View} from 'react-native';
import Markdown from '@components/markdown';
import {Screens} from '@constants';
import {useTheme} from '@context/theme';
import {getMarkdownTextStyles, getMarkdownBlockStyles} from '@utils/markdown';
import {makeStyleSheetFromTheme} from '@utils/theme';
@ -41,7 +42,7 @@ function DialogIntroductionText({value}: Props) {
disableHashtags={true}
disableAtMentions={true}
disableChannelLink={true}
location=''
location={Screens.INTERACTIVE_DIALOG}
theme={theme}
/>
</View>

View file

@ -323,6 +323,7 @@ export default function ManageChannelMembers({
tutorialWatched={tutorialWatched}
includeUserMargin={true}
fetchMore={handleReachedBottom}
location={Screens.MANAGE_CHANNEL_MEMBERS}
/>
</SafeAreaView>
);

View file

@ -0,0 +1,107 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {DeviceEventEmitter, Keyboard, type EmitterSubscription} from 'react-native';
import {Navigation} from 'react-native-navigation';
import {Events, Preferences, Screens} from '@constants';
import NavigationStore from '@store/navigation_store';
import {openAsBottomSheet, openUserProfileModal} from './navigation';
import type {FirstArgument} from '@typings/utils/utils';
import type {IntlShape} from 'react-intl';
function expectShowModalCalledWith(screen: string, title: string, props?: Record<string, unknown>) {
expect(Navigation.showModal).toHaveBeenCalledWith({
stack: {
children: [{
component: {
id: screen,
name: screen,
passProps: {
...props,
isModal: true,
},
options: expect.any(Object),
},
}],
},
});
}
function expectShowModalOverCurrentContext(screen: string, props?: Record<string, unknown>) {
expectShowModalCalledWith(screen, '', props);
}
function expectOpenAsBottomSheetCalledWith(props: FirstArgument<typeof openAsBottomSheet>, isTablet: boolean) {
if (isTablet) {
expectShowModalCalledWith(props.screen, props.title, {closeButtonId: props.closeButtonId, ...props.props});
} else {
expectShowModalOverCurrentContext(props.screen, props.props);
}
}
function expectDismissBottomSheetCalledWith(screenToDismiss: string, listenerCallback: jest.Mock) {
expect(listenerCallback).toHaveBeenCalled();
expect(NavigationStore.waitUntilScreensIsRemoved).toHaveBeenCalledWith(screenToDismiss);
}
function expectNotDismissBottomSheetCalledWith(listenerCallback: jest.Mock) {
expect(listenerCallback).not.toHaveBeenCalled();
expect(NavigationStore.waitUntilScreensIsRemoved).not.toHaveBeenCalled();
}
describe('openUserProfileModal', () => {
const intl = {
formatMessage: jest.fn(({defaultMessage}) => defaultMessage),
} as unknown as IntlShape;
const theme = Preferences.THEMES.denim;
const props = {
userId: 'user123',
};
let eventSubscription: EmitterSubscription;
const listenerCallback = jest.fn();
beforeAll(() => {
eventSubscription = DeviceEventEmitter.addListener(Events.CLOSE_BOTTOM_SHEET, listenerCallback);
jest.spyOn(NavigationStore, 'waitUntilScreensIsRemoved').mockImplementation();
});
beforeEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
eventSubscription.remove();
});
it('should dismiss the keyboard', () => {
openUserProfileModal(intl, theme, props);
expect(Keyboard.dismiss).toHaveBeenCalled();
});
it('should dismiss the bottom sheet if screenToDismiss is provided', async () => {
const screenToDismiss = Screens.BOTTOM_SHEET;
await openUserProfileModal(intl, theme, props, screenToDismiss);
expectDismissBottomSheetCalledWith(screenToDismiss, listenerCallback);
expectOpenAsBottomSheetCalledWith({
screen: Screens.USER_PROFILE,
title: 'Profile',
closeButtonId: 'close-user-profile',
theme,
props,
}, false);
});
it('should not call dismiss if no screenToDismiss is provided', async () => {
await openUserProfileModal(intl, theme, props);
expectNotDismissBottomSheetCalledWith(listenerCallback);
expectOpenAsBottomSheetCalledWith({
screen: Screens.USER_PROFILE,
title: 'Profile',
closeButtonId: 'close-user-profile',
theme,
props,
}, false);
});
});

View file

@ -4,7 +4,7 @@
/* eslint-disable max-lines */
import merge from 'deepmerge';
import {Appearance, DeviceEventEmitter, StatusBar, Platform, Alert, type EmitterSubscription} from 'react-native';
import {Appearance, DeviceEventEmitter, StatusBar, Platform, Alert, type EmitterSubscription, Keyboard} 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';
@ -20,8 +20,11 @@ import {appearanceControlledScreens, mergeNavigationOptions} from '@utils/naviga
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';
const alpha = {
from: 0,
@ -889,3 +892,20 @@ export async function findChannels(title: string, theme: Theme) {
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}});
}

View file

@ -13,10 +13,11 @@ import EmojiBar from './emoji_bar';
import ReactorsList from './reactors_list';
import type ReactionModel from '@typings/database/models/servers/reaction';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
initialEmoji: string;
location: string;
location: AvailableScreens;
reactions?: ReactionModel[];
}

View file

@ -13,9 +13,10 @@ import {useBottomSheetListsFix} from '@hooks/bottom_sheet_lists_fix';
import Reactor from './reactor';
import type ReactionModel from '@typings/database/models/servers/reaction';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
location: string;
location: AvailableScreens;
reactions: ReactionModel[];
type?: BottomSheetList;
}

View file

@ -3,21 +3,21 @@
import React, {useEffect} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard} from 'react-native';
import {fetchUsersByIds} from '@actions/remote/user';
import UserItem from '@components/user_item';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {dismissBottomSheet, openAsBottomSheet} from '@screens/navigation';
import {openUserProfileModal} from '@screens/navigation';
import type ReactionModel from '@typings/database/models/servers/reaction';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
channelId: string;
location: string;
location: AvailableScreens;
reaction: ReactionModel;
user?: UserModel;
}
@ -28,14 +28,11 @@ const Reactor = ({channelId, location, reaction, user}: Props) => {
const serverUrl = useServerUrl();
const openUserProfile = async () => {
if (user) {
await dismissBottomSheet(Screens.REACTIONS);
const screen = Screens.USER_PROFILE;
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const closeButtonId = 'close-user-profile';
const props = {closeButtonId, location, userId: user.id, channelId};
Keyboard.dismiss();
openAsBottomSheet({screen, title, theme, closeButtonId, props});
openUserProfileModal(intl, theme, {
userId: user.id,
channelId,
location,
}, Screens.REACTIONS);
}
};

View file

@ -7,6 +7,7 @@ import {View} from 'react-native';
import {sendTestNotification} from '@actions/remote/notifications';
import SectionNotice from '@components/section_notice';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {useExternalLink} from '@hooks/use_external_link';
import {isMinimumServerVersion} from '@utils/helpers';
@ -131,6 +132,7 @@ const SendTestNotificationNotice = ({
primaryButton={primaryButton}
secondaryButton={secondaryButton}
type='hint'
location={Screens.SETTINGS_NOTIFICATION_PUSH}
/>
</View>
);

View file

@ -36,6 +36,7 @@ jest.mock('react-native-navigation', () => ({
registerComponentDidDisappearListener: jest.fn(),
}),
dismissOverlay: jest.fn(),
setDefaultOptions: jest.fn(),
},
}));

View file

@ -143,6 +143,7 @@ const Thread = ({
testID='thread.post_draft'
containerHeight={containerHeight}
isChannelScreen={false}
location={Screens.THREAD}
/>
</ExtraKeyboardProvider>
}

View file

@ -15,10 +15,12 @@ import {useTheme} from '@context/theme';
import {dismissBottomSheet} from '@screens/navigation';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import type {AvailableScreens} from '@typings/screens/navigation';
export type OptionsType = 'all' | 'message';
type Props = {
location: string;
location?: AvailableScreens;
type: OptionsType;
userId: string;
username: string;

View file

@ -39,7 +39,7 @@ type Props = {
isSystemAdmin: boolean;
isTeamAdmin: boolean;
manageMode?: boolean;
location: AvailableScreens;
location?: AvailableScreens;
teamId: string;
teammateDisplayName: string;
user: UserModel;
@ -91,7 +91,7 @@ const UserProfile = ({
}: Props) => {
const {formatMessage, locale} = useIntl();
const serverUrl = useServerUrl();
const channelContext = channelContextScreens.includes(location);
const channelContext = location ? channelContextScreens.includes(location) : false;
const showOptions: OptionsType = channelContext && !user.isBot ? 'all' : 'message';
const override = Boolean(userIconOverride || usernameOverride);
const {bottom} = useSafeAreaInsets();

View file

@ -21,35 +21,6 @@ import {
alertSlashCommandFailed,
} from './';
jest.mock('react-native', () => {
const RN = jest.requireActual('react-native');
return {
Platform: RN.Platform,
NativeModules: {
...RN.NativeModules,
RNUtils: {
getConstants: () => ({
appGroupIdentifier: 'group.mattermost.rnbeta',
appGroupSharedDirectory: {
sharedDirectory: '',
databasePath: '',
},
}),
addListener: jest.fn(),
removeListeners: jest.fn(),
isRunningInSplitView: jest.fn().mockReturnValue({isSplit: false, isTablet: false}),
getDeliveredNotifications: jest.fn().mockResolvedValue([]),
removeChannelNotifications: jest.fn().mockImplementation(),
removeThreadNotifications: jest.fn().mockImplementation(),
removeServerNotifications: jest.fn().mockImplementation(),
},
},
Alert: {
alert: jest.fn(),
},
};
});
jest.mock('@i18n', () => ({
t: (id: string) => id,
}));

View file

@ -94,6 +94,7 @@ jest.doMock('react-native', () => {
InteractionManager: RNInteractionManager,
NativeModules: RNNativeModules,
Linking: RNLinking,
Keyboard: RNKeyboard,
} = ReactNative;
const Alert = {
@ -250,6 +251,14 @@ jest.doMock('react-native', () => {
}),
};
const Keyboard = {
...RNKeyboard,
dismiss: jest.fn(),
addListener: jest.fn(() => ({
remove: jest.fn(),
})),
};
return Object.setPrototypeOf({
Platform: {
...Platform,
@ -270,6 +279,7 @@ jest.doMock('react-native', () => {
InteractionManager,
NativeModules,
Linking,
Keyboard,
Animated: {
...ReactNative.Animated,
timing: jest.fn(() => ({
@ -393,6 +403,7 @@ jest.mock('react-native-share', () => ({
}));
jest.mock('@screens/navigation', () => ({
...jest.requireActual('@screens/navigation'),
resetToChannel: jest.fn(),
resetToSelectServer: jest.fn(),
resetToTeams: jest.fn(),

4
types/utils/utils.ts Normal file
View file

@ -0,0 +1,4 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type FirstArgument<T> = T extends (...args: infer P) => any ? P[0] : never;