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

View file

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

View file

@ -14,6 +14,7 @@ import {logWarning} from '@utils/log';
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown'; import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
import {concatStyles, changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {concatStyles, changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import type {AvailableScreens} from '@typings/screens/navigation';
import type {PrimitiveType} from 'intl-messageformat'; import type {PrimitiveType} from 'intl-messageformat';
type Props = { type Props = {
@ -21,7 +22,7 @@ type Props = {
channelId?: string; channelId?: string;
defaultMessage: string; defaultMessage: string;
id: string; id: string;
location: string; location: AvailableScreens;
onPostPress?: (e: GestureResponderEvent) => void; onPostPress?: (e: GestureResponderEvent) => void;
style?: StyleProp<TextStyle>; style?: StyleProp<TextStyle>;
values?: Record<string, PrimitiveType>; 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 Clipboard from '@react-native-clipboard/clipboard';
import React, {useCallback, useEffect, useMemo} from 'react'; import React, {useCallback, useEffect, useMemo} from 'react';
import {useIntl} from 'react-intl'; 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 {fetchUserOrGroupsByMentionsInBatch} from '@actions/remote/user';
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item'; import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server'; import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme'; import {useTheme} from '@context/theme';
import GroupModel from '@database/models/server/group'; import GroupModel from '@database/models/server/group';
import {useMemoMentionedGroup, useMemoMentionedUser} from '@hooks/markdown'; 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 {bottomSheetSnapPoint} from '@utils/helpers';
import {displayUsername} from '@utils/user'; import {displayUsername} from '@utils/user';
import type GroupMembershipModel from '@typings/database/models/servers/group_membership'; import type GroupMembershipModel from '@typings/database/models/servers/group_membership';
import type UserModelType from '@typings/database/models/servers/user'; import type UserModelType from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
type AtMentionProps = { type AtMentionProps = {
channelId?: string; channelId?: string;
currentUserId: string; currentUserId: string;
disableAtChannelMentionHighlight?: boolean; disableAtChannelMentionHighlight?: boolean;
isSearchResult?: boolean; isSearchResult?: boolean;
location: string; location: AvailableScreens;
mentionKeys?: Array<{key: string }>; mentionKeys?: Array<{key: string }>;
mentionName: string; mentionName: string;
mentionStyle: StyleProp<TextStyle>; mentionStyle: StyleProp<TextStyle>;
@ -95,13 +95,11 @@ const AtMention = ({
return; return;
} }
const screen = Screens.USER_PROFILE; openUserProfileModal(intl, theme, {
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}); location,
const closeButtonId = 'close-user-profile'; userId: user.id,
const props = {closeButtonId, location, userId: user.id, channelId}; channelId,
});
Keyboard.dismiss();
openAsBottomSheet({screen, title, theme, closeButtonId, props});
}; };
const handleLongPress = useCallback(() => { const handleLongPress = useCallback(() => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -20,11 +20,13 @@ import AttachmentText from './attachment_text';
import AttachmentThumbnail from './attachment_thumbnail'; import AttachmentThumbnail from './attachment_thumbnail';
import AttachmentTitle from './attachment_title'; import AttachmentTitle from './attachment_title';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = { type Props = {
attachment: MessageAttachment; attachment: MessageAttachment;
channelId: string; channelId: string;
layoutWidth?: number; layoutWidth?: number;
location: string; location: AvailableScreens;
metadata?: PostMetadata | null; metadata?: PostMetadata | null;
postId: string; postId: string;
theme: Theme; theme: Theme;
@ -138,6 +140,7 @@ export default function MessageAttachment({attachment, channelId, layoutWidth, l
actions={attachment.actions!} actions={attachment.actions!}
postId={postId} postId={postId}
theme={theme} theme={theme}
location={location}
/> />
} }
{attachment.image_url && Boolean(metadata?.images?.[attachment.image_url]) && {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 PostModel from '@typings/database/models/servers/post';
import type {SearchPattern} from '@typings/global/markdown'; import type {SearchPattern} from '@typings/global/markdown';
import type {AvailableScreens} from '@typings/screens/navigation';
type BodyProps = { type BodyProps = {
appsEnabled: boolean; appsEnabled: boolean;
@ -36,7 +37,7 @@ type BodyProps = {
isPendingOrFailed: boolean; isPendingOrFailed: boolean;
isPostAcknowledgementEnabled?: boolean; isPostAcknowledgementEnabled?: boolean;
isPostAddChannelMember: boolean; isPostAddChannelMember: boolean;
location: string; location: AvailableScreens;
post: PostModel; post: PostModel;
searchPatterns?: SearchPattern[]; searchPatterns?: SearchPattern[];
showAddReaction?: boolean; 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 PostModel from '@typings/database/models/servers/post';
import type UserModel from '@typings/database/models/servers/user'; import type UserModel from '@typings/database/models/servers/user';
import type {HighlightWithoutNotificationKey, SearchPattern, UserMentionKey} from '@typings/global/markdown'; import type {HighlightWithoutNotificationKey, SearchPattern, UserMentionKey} from '@typings/global/markdown';
import type {AvailableScreens} from '@typings/screens/navigation';
type MessageProps = { type MessageProps = {
currentUser?: UserModel; currentUser?: UserModel;
@ -27,7 +28,7 @@ type MessageProps = {
isPendingOrFailed: boolean; isPendingOrFailed: boolean;
isReplyPost: boolean; isReplyPost: boolean;
layoutWidth?: number; layoutWidth?: number;
location: string; location: AvailableScreens;
post: PostModel; post: PostModel;
searchPatterns?: SearchPattern[]; searchPatterns?: SearchPattern[];
theme: Theme; theme: Theme;

View file

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

View file

@ -3,21 +3,22 @@
import React, {useCallback} from 'react'; import React, {useCallback} from 'react';
import {useIntl} from 'react-intl'; 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 CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
import FormattedText from '@components/formatted_text'; import FormattedText from '@components/formatted_text';
import {Screens} from '@constants'; import {usePreventDoubleTap} from '@hooks/utils';
import {openAsBottomSheet} from '@screens/navigation'; import {openUserProfileModal} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
import {makeStyleSheetFromTheme} from '@utils/theme'; import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography'; import {typography} from '@utils/typography';
import type {AvailableScreens} from '@typings/screens/navigation';
type HeaderDisplayNameProps = { type HeaderDisplayNameProps = {
channelId: string; channelId: string;
commentCount: number; commentCount: number;
displayName?: string; displayName?: string;
location: string; location: AvailableScreens;
rootPostAuthor?: string; rootPostAuthor?: string;
shouldRenderReplyButton?: boolean; shouldRenderReplyButton?: boolean;
theme: Theme; theme: Theme;
@ -71,15 +72,15 @@ const HeaderDisplayName = ({
const intl = useIntl(); const intl = useIntl();
const style = getStyleSheet(theme); const style = getStyleSheet(theme);
const onPress = useCallback(preventDoubleTap(() => { const onPress = usePreventDoubleTap(useCallback(() => {
const screen = Screens.USER_PROFILE; openUserProfileModal(intl, theme, {
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}); location,
const closeButtonId = 'close-user-profile'; userId,
const props = {closeButtonId, userId, channelId, location, userIconOverride, usernameOverride}; channelId,
userIconOverride,
Keyboard.dismiss(); usernameOverride,
openAsBottomSheet({screen, title, theme, closeButtonId, props}); });
}), [intl.locale, channelId, userIconOverride, userId, usernameOverride, theme]); }, [intl, userId, channelId, location, userIconOverride, usernameOverride, theme]));
const calcNameWidth = () => { const calcNameWidth = () => {
const isLandscape = dimensions.width > dimensions.height; 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 PostModel from '@typings/database/models/servers/post';
import type UserModel from '@typings/database/models/servers/user'; import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
type HeaderProps = { type HeaderProps = {
author?: UserModel; author?: UserModel;
@ -37,7 +38,7 @@ type HeaderProps = {
isPendingOrFailed: boolean; isPendingOrFailed: boolean;
isSystemPost: boolean; isSystemPost: boolean;
isWebHook: boolean; isWebHook: boolean;
location: string; location: AvailableScreens;
post: PostModel; post: PostModel;
rootPostAuthor?: UserModel; rootPostAuthor?: UserModel;
showPostPriority: boolean; 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 ThreadModel from '@typings/database/models/servers/thread';
import type UserModel from '@typings/database/models/servers/user'; import type UserModel from '@typings/database/models/servers/user';
import type {SearchPattern} from '@typings/global/markdown'; import type {SearchPattern} from '@typings/global/markdown';
import type {AvailableScreens} from '@typings/screens/navigation';
type PostProps = { type PostProps = {
appsEnabled: boolean; appsEnabled: boolean;
@ -57,7 +58,7 @@ type PostProps = {
isLastReply?: boolean; isLastReply?: boolean;
isPostAddChannelMember: boolean; isPostAddChannelMember: boolean;
isPostPriorityEnabled: boolean; isPostPriorityEnabled: boolean;
location: string; location: AvailableScreens;
post: PostModel; post: PostModel;
rootId?: string; rootId?: string;
previousPost?: PostModel; previousPost?: PostModel;

View file

@ -17,11 +17,12 @@ import {typography} from '@utils/typography';
import type PostModel from '@typings/database/models/servers/post'; import type PostModel from '@typings/database/models/servers/post';
import type UserModel from '@typings/database/models/servers/user'; import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
import type {PrimitiveType} from 'intl-messageformat'; import type {PrimitiveType} from 'intl-messageformat';
type SystemMessageProps = { type SystemMessageProps = {
author?: UserModel; author?: UserModel;
location: string; location: AvailableScreens;
post: PostModel; 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 {PostListItem, PostListOtherItem, ViewableItemsChanged, ViewableItemsChangedListenerEvent} from '@typings/components/post_list';
import type PostModel from '@typings/database/models/servers/post'; import type PostModel from '@typings/database/models/servers/post';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = { type Props = {
appsEnabled: boolean; appsEnabled: boolean;
@ -40,7 +41,7 @@ type Props = {
isCRTEnabled?: boolean; isCRTEnabled?: boolean;
isPostAcknowledgementEnabled?: boolean; isPostAcknowledgementEnabled?: boolean;
lastViewedAt: number; lastViewedAt: number;
location: string; location: AvailableScreens;
nativeID: string; nativeID: string;
onEndReached?: () => void; onEndReached?: () => void;
posts: PostModel[]; posts: PostModel[];

View file

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

View file

@ -3,6 +3,7 @@
import React, {type ComponentProps} from 'react'; import React, {type ComponentProps} from 'react';
import {Screens} from '@constants';
import {fireEvent, renderWithEverything} from '@test/intl-test-helper'; import {fireEvent, renderWithEverything} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper'; import TestHelper from '@test/test_helper';
@ -38,6 +39,7 @@ function getBaseProps(): ComponentProps<typeof SectionNotice> {
loading: true, loading: true,
}, },
onDismissClick: jest.fn(), 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 SectionNoticeButton from './section_notice_button';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = { type Props = {
title: string; title: string;
text?: string; text?: string;
@ -22,6 +24,7 @@ type Props = {
type?: 'info' | 'success' | 'danger' | 'welcome' | 'warning' | 'hint'; type?: 'info' | 'success' | 'danger' | 'welcome' | 'warning' | 'hint';
isDismissable?: boolean; isDismissable?: boolean;
onDismissClick?: () => void; onDismissClick?: () => void;
location: AvailableScreens;
} }
const iconByType = { const iconByType = {
@ -150,6 +153,7 @@ const SectionNotice = ({
secondaryButton, secondaryButton,
text, text,
type = 'info', type = 'info',
location,
}: Props) => { }: Props) => {
const theme = useTheme(); const theme = useTheme();
const styles = getStyleFromTheme(theme); const styles = getStyleFromTheme(theme);
@ -179,7 +183,7 @@ const SectionNotice = ({
{text && ( {text && (
<Markdown <Markdown
theme={theme} theme={theme}
location='' location={location}
baseTextStyle={styles.baseText} baseTextStyle={styles.baseText}
value={text} value={text}
/> />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,7 +25,7 @@ import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager'; import NetworkManager from '@managers/network_manager';
import {queryAllActiveServers} from '@queries/app/servers'; import {queryAllActiveServers} from '@queries/app/servers';
import {getCurrentUser} from '@queries/servers/user'; 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'; import {useTryCallsFunction, usePermissionsChecker, useCallsAdjustment, useHostControlsAvailable, useHostMenus} from './hooks';
@ -74,6 +74,7 @@ jest.mock('@queries/servers/user', () => ({
jest.mock('@screens/navigation', () => ({ jest.mock('@screens/navigation', () => ({
openAsBottomSheet: jest.fn(), openAsBottomSheet: jest.fn(),
openUserProfileModal: jest.fn(),
})); }));
describe('Calls Hooks', () => { describe('Calls Hooks', () => {
@ -267,9 +268,13 @@ describe('Calls Hooks', () => {
await result.current.onPress(mockSession)(); await result.current.onPress(mockSession)();
}); });
expect(openAsBottomSheet).toHaveBeenCalledWith(expect.objectContaining({ expect(openUserProfileModal).toHaveBeenCalledWith(
screen: Screens.USER_PROFILE, 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 NetworkManager from '@managers/network_manager';
import {queryAllActiveServers} from '@queries/app/servers'; import {queryAllActiveServers} from '@queries/app/servers';
import {getCurrentUser} from '@queries/servers/user'; import {getCurrentUser} from '@queries/servers/user';
import {openAsBottomSheet} from '@screens/navigation'; import {openAsBottomSheet, openUserProfileModal} from '@screens/navigation';
import {getFullErrorMessage} from '@utils/errors'; import {getFullErrorMessage} from '@utils/errors';
import {isSystemAdmin} from '@utils/user'; import {isSystemAdmin} from '@utils/user';
@ -211,12 +211,9 @@ export const useHostMenus = () => {
}, [intl, theme]); }, [intl, theme]);
const openUserProfile = useCallback(async (session: CallSession) => { const openUserProfile = useCallback(async (session: CallSession) => {
const screen = Screens.USER_PROFILE; openUserProfileModal(intl, theme, {
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}); userId: session.userId,
const closeUserProfile = 'close-user-profile'; });
const props = {closeButtonId: closeUserProfile, location: '', userId: session.userId};
openAsBottomSheet({screen, title, theme, closeButtonId: closeUserProfile, props});
}, [intl, theme]); }, [intl, theme]);
const onPress = useCallback((session: CallSession) => () => { const onPress = useCallback((session: CallSession) => () => {

View file

@ -113,6 +113,7 @@ jest.mock('react-native-navigation', () => ({
pop: jest.fn(() => Promise.resolve({ pop: jest.fn(() => Promise.resolve({
catch: jest.fn(), 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 {handleGotoLocation} from '@actions/remote/command';
import CompassIcon from '@components/compass_icon'; import CompassIcon from '@components/compass_icon';
import Markdown from '@components/markdown'; import Markdown from '@components/markdown';
import {Screens} from '@constants';
import {AppCallResponseTypes} from '@constants/apps'; import {AppCallResponseTypes} from '@constants/apps';
import {useServerUrl} from '@context/server'; import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme'; import {useTheme} from '@context/theme';
@ -401,7 +402,7 @@ function AppsFormComponent({
baseTextStyle={style.errorLabel} baseTextStyle={style.errorLabel}
textStyles={getMarkdownTextStyles(theme)} textStyles={getMarkdownTextStyles(theme)}
blockStyles={getMarkdownBlockStyles(theme)} blockStyles={getMarkdownBlockStyles(theme)}
location='' location={Screens.APPS_FORM}
disableAtMentions={true} disableAtMentions={true}
value={error} value={error}
theme={theme} theme={theme}

View file

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

View file

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

View file

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

View file

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

View file

@ -6,6 +6,7 @@ import React, {useCallback, useMemo, useState} from 'react';
import AutocompleteSelector from '@components/autocomplete_selector'; import AutocompleteSelector from '@components/autocomplete_selector';
import BoolSetting from '@components/settings/bool_setting'; import BoolSetting from '@components/settings/bool_setting';
import TextSetting from '@components/settings/text_setting'; import TextSetting from '@components/settings/text_setting';
import {Screens} from '@constants';
type HookResult<T> = [ type HookResult<T> = [
{[x: string]: T}, {[x: string]: T},
@ -28,6 +29,7 @@ export const useStringProp = (
secureTextEntry={false} secureTextEntry={false}
testID={`${propName}.input`} testID={`${propName}.input`}
value={value} value={value}
location={Screens.COMPONENT_LIBRARY}
/> />
), [value, propName, isTextarea]); ), [value, propName, isTextarea]);
const preparedProp = useMemo(() => ({[propName]: value}), [propName, value]); const preparedProp = useMemo(() => ({[propName]: value}), [propName, value]);
@ -46,6 +48,7 @@ export const useBooleanProp = (
testID={`${propName}.input`} testID={`${propName}.input`}
value={value} value={value}
label={propName} label={propName}
location={Screens.COMPONENT_LIBRARY}
/> />
), [propName, value]); ), [propName, value]);
const preparedProp = useMemo(() => ({[propName]: value}), [propName, value]); const preparedProp = useMemo(() => ({[propName]: value}), [propName, value]);
@ -99,6 +102,7 @@ export const useDropdownProp = (
onSelected={onChange} onSelected={onChange}
options={renderedOptions} options={renderedOptions}
selected={value} selected={value}
location={Screens.COMPONENT_LIBRARY}
/> />
), [onChange, propName, renderedOptions, value]); ), [onChange, propName, renderedOptions, value]);
const preparedProp = useMemo(() => (value === ALL_OPTION ? undefined : ({[propName]: value})), [propName, 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 {ScrollView, View, type StyleProp, type ViewStyle} from 'react-native';
import AutocompleteSelector from '@components/autocomplete_selector'; import AutocompleteSelector from '@components/autocomplete_selector';
import {Preferences} from '@constants'; import {Preferences, Screens} from '@constants';
import {CustomThemeProvider} from '@context/theme'; import {CustomThemeProvider} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import SecurityManager from '@managers/security_manager'; import SecurityManager from '@managers/security_manager';
@ -122,6 +122,7 @@ const ComponentLibrary = ({componentId}: Props) => {
onSelected={onSelectComponent} onSelected={onSelectComponent}
selected={selectedComponent} selected={selectedComponent}
options={componentOptions} options={componentOptions}
location={Screens.COMPONENT_LIBRARY}
/> />
<AutocompleteSelector <AutocompleteSelector
testID='selectedTheme' testID='selectedTheme'
@ -129,6 +130,7 @@ const ComponentLibrary = ({componentId}: Props) => {
onSelected={onSelectTheme} onSelected={onSelectTheme}
selected={selectedTheme} selected={selectedTheme}
options={themeOptions} options={themeOptions}
location={Screens.COMPONENT_LIBRARY}
/> />
<AutocompleteSelector <AutocompleteSelector
testID='selectedBackground' testID='selectedBackground'
@ -136,6 +138,7 @@ const ComponentLibrary = ({componentId}: Props) => {
onSelected={onSelectBackground} onSelected={onSelectBackground}
selected={selectedBackground} selected={selectedBackground}
options={backgroundOptions} options={backgroundOptions}
location={Screens.COMPONENT_LIBRARY}
/> />
<View style={backgroundStyle}> <View style={backgroundStyle}>
<CustomThemeProvider theme={Preferences.THEMES[selectedTheme]}> <CustomThemeProvider theme={Preferences.THEMES[selectedTheme]}>

View file

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

View file

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

View file

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

View file

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

View file

@ -21,10 +21,11 @@ import DraftAndScheduledPostSwipeActions from '../draft_and_scheduled_post_swipe
import ScheduledPostEmptyComponent from '../scheduled_post_empty_component'; import ScheduledPostEmptyComponent from '../scheduled_post_empty_component';
import type ScheduledPostModel from '@typings/database/models/servers/scheduled_post'; import type ScheduledPostModel from '@typings/database/models/servers/scheduled_post';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = { type Props = {
allScheduledPosts: ScheduledPostModel[]; allScheduledPosts: ScheduledPostModel[];
location: string; location: AvailableScreens;
tutorialWatched: boolean; 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 PostModel from '@typings/database/models/servers/post';
import type ThreadModel from '@typings/database/models/servers/thread'; import type ThreadModel from '@typings/database/models/servers/thread';
import type UserModel from '@typings/database/models/servers/user'; import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = { type Props = {
author?: UserModel; author?: UserModel;
channel?: ChannelModel; channel?: ChannelModel;
location: string; location: AvailableScreens;
post?: PostModel; post?: PostModel;
teammateNameDisplay: string; teammateNameDisplay: string;
testID: 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 ThreadModel from '@typings/database/models/servers/thread';
import type UserModel from '@typings/database/models/servers/user'; import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = { type Props = {
author?: UserModel; author?: UserModel;
channelId: string; channelId: string;
location: string; location: AvailableScreens;
participants: UserModel[]; participants: UserModel[];
testID: string; testID: string;
thread: ThreadModel; thread: ThreadModel;

View file

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

View file

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

View file

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

View file

@ -323,6 +323,7 @@ export default function ManageChannelMembers({
tutorialWatched={tutorialWatched} tutorialWatched={tutorialWatched}
includeUserMargin={true} includeUserMargin={true}
fetchMore={handleReachedBottom} fetchMore={handleReachedBottom}
location={Screens.MANAGE_CHANNEL_MEMBERS}
/> />
</SafeAreaView> </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 */ /* eslint-disable max-lines */
import merge from 'deepmerge'; 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 {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 tinyColor from 'tinycolor2';
@ -20,8 +20,11 @@ import {appearanceControlledScreens, mergeNavigationOptions} from '@utils/naviga
import {changeOpacity, setNavigatorStyles} from '@utils/theme'; import {changeOpacity, setNavigatorStyles} from '@utils/theme';
import type {BottomSheetFooterProps} from '@gorhom/bottom-sheet'; import type {BottomSheetFooterProps} from '@gorhom/bottom-sheet';
import type {default as UserProfileScreen} from '@screens/user_profile';
import type {LaunchProps} from '@typings/launch'; import type {LaunchProps} from '@typings/launch';
import type {AvailableScreens, NavButtons} from '@typings/screens/navigation'; import type {AvailableScreens, NavButtons} from '@typings/screens/navigation';
import type {ComponentProps} from 'react';
import type {IntlShape} from 'react-intl';
const alpha = { const alpha = {
from: 0, from: 0,
@ -889,3 +892,20 @@ export async function findChannels(title: string, theme: Theme) {
options, 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 ReactorsList from './reactors_list';
import type ReactionModel from '@typings/database/models/servers/reaction'; import type ReactionModel from '@typings/database/models/servers/reaction';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = { type Props = {
initialEmoji: string; initialEmoji: string;
location: string; location: AvailableScreens;
reactions?: ReactionModel[]; reactions?: ReactionModel[];
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,35 +21,6 @@ import {
alertSlashCommandFailed, alertSlashCommandFailed,
} from './'; } 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', () => ({ jest.mock('@i18n', () => ({
t: (id: string) => id, t: (id: string) => id,
})); }));

View file

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