[Gekidou] user profile (#6353)
* User profile * Access User Profile from reaction list * Fix threads participants list & open user profile on tap * Extra bottom padding * Profile long press tutorial * Adjust heights * Reduce label margin from 12 to 8
This commit is contained in:
parent
e01d8f46c2
commit
2f07b7afc8
75 changed files with 1428 additions and 298 deletions
|
|
@ -29,3 +29,16 @@ export const storeMultiServerTutorial = async (prepareRecordsOnly = false) => {
|
|||
prepareRecordsOnly,
|
||||
});
|
||||
};
|
||||
|
||||
export const storeProfileLongPressTutorial = async (prepareRecordsOnly = false) => {
|
||||
const operator = DatabaseManager.appDatabase?.operator;
|
||||
|
||||
if (!operator) {
|
||||
return {error: 'No App database found'};
|
||||
}
|
||||
|
||||
return operator.handleGlobal({
|
||||
globals: [{id: GLOBAL_IDENTIFIERS.PROFILE_LONG_PRESS_TUTORIAL, value: 'true'}],
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
|||
import General from '@constants/general';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getRecentCustomStatuses} from '@queries/servers/system';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {getCurrentUser, getUserById} from '@queries/servers/user';
|
||||
|
||||
import {addRecentReaction} from './reactions';
|
||||
|
||||
|
|
@ -141,3 +141,27 @@ export const updateLocalUser = async (
|
|||
|
||||
return {};
|
||||
};
|
||||
|
||||
export const storeProfile = async (serverUrl: string, profile: UserProfile) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const {database} = operator;
|
||||
const user = await getUserById(database, profile.id);
|
||||
if (user) {
|
||||
return {user};
|
||||
}
|
||||
|
||||
const records = await operator.handleUsers({
|
||||
users: [profile],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
return {user: records[0]};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -797,6 +797,13 @@ export async function createDirectChannel(serverUrl: string, userId: string, dis
|
|||
if (!currentUser) {
|
||||
return {error: 'Cannot get the current user'};
|
||||
}
|
||||
|
||||
const channelName = getDirectChannelName(currentUser.id, userId);
|
||||
const channel = await getChannelByName(database, channelName);
|
||||
if (channel) {
|
||||
return {data: channel.toApi()};
|
||||
}
|
||||
|
||||
EphemeralStore.creatingDMorGMTeammates = [userId];
|
||||
const created = await client.createDirectChannel([userId, currentUser.id]);
|
||||
const profiles: UserProfile[] = [];
|
||||
|
|
|
|||
|
|
@ -822,3 +822,43 @@ export const autoUpdateTimezone = async (serverUrl: string, {deviceTimezone, use
|
|||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const fetchTeamAndChannelMembership = async (serverUrl: string, userId: string, teamId: string, channelId?: string) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl].operator;
|
||||
if (!operator) {
|
||||
return {error: `No database present for ${serverUrl}`};
|
||||
}
|
||||
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const requests = await Promise.all([
|
||||
client.getTeamMember(teamId, userId),
|
||||
channelId ? client.getChannelMember(channelId, userId) : undefined,
|
||||
]);
|
||||
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
modelPromises.push(operator.handleTeamMemberships({
|
||||
teamMemberships: [requests[0]],
|
||||
prepareRecordsOnly: true,
|
||||
}));
|
||||
const channelMemberships = requests[1];
|
||||
if (channelMemberships) {
|
||||
modelPromises.push(operator.handleChannelMembership({
|
||||
channelMemberships: [channelMemberships],
|
||||
prepareRecordsOnly: true,
|
||||
}));
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -159,6 +159,11 @@ export async function handleChannelMemberUpdatedEvent(serverUrl: string, msg: an
|
|||
settings: [updatedChannelMember],
|
||||
prepareRecordsOnly: true,
|
||||
}));
|
||||
|
||||
models.push(...await operator.handleChannelMembership({
|
||||
channelMemberships: [updatedChannelMember],
|
||||
prepareRecordsOnly: true,
|
||||
}));
|
||||
const rolesRequest = await fetchRolesIfNeeded(serverUrl, updatedChannelMember.roles.split(','), true);
|
||||
if (rolesRequest.roles?.length) {
|
||||
models.push(...await operator.handleRole({roles: rolesRequest.roles, prepareRecordsOnly: true}));
|
||||
|
|
|
|||
|
|
@ -111,6 +111,13 @@ export async function handleTeamMemberRoleUpdatedEvent(serverUrl: string, msg: W
|
|||
|
||||
models.push(...myTeamRecords);
|
||||
|
||||
// update TeamMembership table
|
||||
const teamMembership = await operator.handleTeamMemberships({
|
||||
teamMemberships: [member],
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
models.push(...teamMembership);
|
||||
|
||||
await operator.batchRecords(models);
|
||||
} catch {
|
||||
// do nothing
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ const enhance = withObservables(['channel', 'showTeamName'], ({channel, database
|
|||
|
||||
let membersCount = of$(0);
|
||||
if (channel.type === General.GM_CHANNEL) {
|
||||
membersCount = channel.members.observeCount();
|
||||
membersCount = channel.members.observeCount(false);
|
||||
}
|
||||
|
||||
const isUnread = myChannel.pipe(
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@ import type {PrimitiveType} from 'intl-messageformat';
|
|||
|
||||
type Props = {
|
||||
baseTextStyle: StyleProp<TextStyle>;
|
||||
channelId?: string;
|
||||
defaultMessage: string;
|
||||
id: string;
|
||||
location: string;
|
||||
onPostPress?: (e: GestureResponderEvent) => void;
|
||||
style?: StyleProp<TextStyle>;
|
||||
textStyles: {
|
||||
|
|
@ -47,7 +49,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
const FormattedMarkdownText = ({baseTextStyle, defaultMessage, id, onPostPress, style, textStyles, values}: Props) => {
|
||||
const FormattedMarkdownText = ({baseTextStyle, channelId, defaultMessage, id, location, onPostPress, style, textStyles, values}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
|
@ -84,8 +86,10 @@ const FormattedMarkdownText = ({baseTextStyle, defaultMessage, id, onPostPress,
|
|||
const renderAtMention = ({context, mentionName}: {context: string[]; mentionName: string}) => {
|
||||
return (
|
||||
<AtMention
|
||||
channelId={channelId}
|
||||
mentionStyle={textStyles.mention}
|
||||
mentionName={mentionName}
|
||||
location={location}
|
||||
onPostPress={onPostPress}
|
||||
textStyle={[computeTextStyle(baseTextStyle, context), styles.atMentionOpacity]}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -6,25 +6,27 @@ import {Database} from '@nozbe/watermelondb';
|
|||
import Clipboard from '@react-native-community/clipboard';
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {GestureResponderEvent, StyleProp, StyleSheet, Text, TextStyle, View} from 'react-native';
|
||||
import {GestureResponderEvent, Keyboard, StyleProp, StyleSheet, Text, TextStyle, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
|
||||
import {Screens} from '@constants';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {useTheme} from '@context/theme';
|
||||
import UserModel from '@database/models/server/user';
|
||||
import {bottomSheet, dismissBottomSheet, showModal} from '@screens/navigation';
|
||||
import {bottomSheet, dismissBottomSheet, openAsBottomSheet} from '@screens/navigation';
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
import {displayUsername, getUsersByUsername} from '@utils/user';
|
||||
|
||||
import type UserModelType from '@typings/database/models/servers/user';
|
||||
|
||||
type AtMentionProps = {
|
||||
channelId?: string;
|
||||
currentUserId: string;
|
||||
database: Database;
|
||||
disableAtChannelMentionHighlight?: boolean;
|
||||
isSearchResult?: boolean;
|
||||
location: string;
|
||||
mentionKeys?: Array<{key: string }>;
|
||||
mentionName: string;
|
||||
mentionStyle: TextStyle;
|
||||
|
|
@ -41,10 +43,12 @@ const style = StyleSheet.create({
|
|||
});
|
||||
|
||||
const AtMention = ({
|
||||
channelId,
|
||||
currentUserId,
|
||||
database,
|
||||
disableAtChannelMentionHighlight,
|
||||
isSearchResult,
|
||||
location,
|
||||
mentionName,
|
||||
mentionKeys,
|
||||
mentionStyle,
|
||||
|
|
@ -89,26 +93,14 @@ const AtMention = ({
|
|||
return user.mentionKeys;
|
||||
}, [currentUserId, mentionKeys, user]);
|
||||
|
||||
const goToUserProfile = () => {
|
||||
const screen = 'UserProfile';
|
||||
const openUserProfile = () => {
|
||||
const screen = Screens.USER_PROFILE;
|
||||
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
|
||||
const passProps = {
|
||||
userId: user.id,
|
||||
};
|
||||
const closeButtonId = 'close-user-profile';
|
||||
const props = {closeButtonId, location, userId: user.id, channelId};
|
||||
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: 'close-settings',
|
||||
icon: closeButton,
|
||||
testID: 'close.settings.button',
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
showModal(screen, title, passProps, options);
|
||||
Keyboard.dismiss();
|
||||
openAsBottomSheet({screen, title, theme, closeButtonId, props});
|
||||
};
|
||||
|
||||
const handleLongPress = useCallback(() => {
|
||||
|
|
@ -196,7 +188,7 @@ const AtMention = ({
|
|||
|
||||
if (canPress) {
|
||||
onLongPress = handleLongPress;
|
||||
onPress = (isSearchResult ? onPostPress : goToUserProfile);
|
||||
onPress = (isSearchResult ? onPostPress : openUserProfile);
|
||||
}
|
||||
|
||||
if (suffix) {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ type MarkdownProps = {
|
|||
autolinkedUrlSchemes?: string[];
|
||||
baseTextStyle: StyleProp<TextStyle>;
|
||||
blockStyles?: MarkdownBlockStyles;
|
||||
channelId?: string;
|
||||
channelMentions?: ChannelMentions;
|
||||
disableAtChannelMentionHighlight?: boolean;
|
||||
disableAtMentions?: boolean;
|
||||
|
|
@ -57,7 +58,7 @@ type MarkdownProps = {
|
|||
isSearchResult?: boolean;
|
||||
layoutHeight?: number;
|
||||
layoutWidth?: number;
|
||||
location?: string;
|
||||
location: string;
|
||||
mentionKeys?: UserMentionKey[];
|
||||
minimumHashtagLength?: number;
|
||||
onPostPress?: (event: GestureResponderEvent) => void;
|
||||
|
|
@ -124,7 +125,7 @@ const computeTextStyle = (textStyles: MarkdownTextStyles, baseStyle: StyleProp<T
|
|||
};
|
||||
|
||||
const Markdown = ({
|
||||
autolinkedUrlSchemes, baseTextStyle, blockStyles, channelMentions,
|
||||
autolinkedUrlSchemes, baseTextStyle, blockStyles, channelId, channelMentions,
|
||||
disableAtChannelMentionHighlight, disableAtMentions, disableBlockQuote, disableChannelLink,
|
||||
disableCodeBlock, disableGallery, disableHashtags, disableHeading, disableTables,
|
||||
enableInlineLatex, enableLatex,
|
||||
|
|
@ -146,10 +147,12 @@ const Markdown = ({
|
|||
|
||||
return (
|
||||
<AtMention
|
||||
channelId={channelId}
|
||||
disableAtChannelMentionHighlight={disableAtChannelMentionHighlight}
|
||||
mentionStyle={textStyles.mention}
|
||||
textStyle={[computeTextStyle(textStyles, baseTextStyle, context), style.atMentionOpacity]}
|
||||
isSearchResult={isSearchResult}
|
||||
location={location}
|
||||
mentionName={mentionName}
|
||||
onPostPress={onPostPress}
|
||||
mentionKeys={mentionKeys}
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ export default function Archived({
|
|||
style={style.archivedText}
|
||||
baseTextStyle={style.baseTextStyle}
|
||||
textStyles={style.textStyles}
|
||||
location=''
|
||||
/>
|
||||
<Button
|
||||
containerStyle={style.closeButton}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@ import PasteableTextInput, {PastedFile, PasteInputRef} from '@mattermost/react-n
|
|||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {IntlShape, useIntl} from 'react-intl';
|
||||
import {
|
||||
Alert, AppState, AppStateStatus, EmitterSubscription, Keyboard,
|
||||
Alert, AppState, AppStateStatus, DeviceEventEmitter, EmitterSubscription, Keyboard,
|
||||
KeyboardTypeOptions, NativeSyntheticEvent, Platform, TextInputSelectionChangeEventData,
|
||||
} from 'react-native';
|
||||
import HWKeyboardEvent from 'react-native-hw-keyboard-event';
|
||||
|
||||
import {updateDraftMessage} from '@actions/local/draft';
|
||||
import {userTyping} from '@actions/websocket/users';
|
||||
import {Screens} from '@constants';
|
||||
import {Events, Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
|
|
@ -266,6 +266,19 @@ export default function PostInput({
|
|||
};
|
||||
}, [onAppStateChange]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = DeviceEventEmitter.addListener(Events.SEND_TO_POST_DRAFT, ({text, location}: {text: string; location: string}) => {
|
||||
const sourceScreen = channelId && rootId ? Screens.THREAD : Screens.CHANNEL;
|
||||
if (location === sourceScreen) {
|
||||
const draft = value ? `${value} ${text} ` : `${text} `;
|
||||
updateValue(draft);
|
||||
updateCursorPosition(draft.length);
|
||||
input.current?.focus();
|
||||
}
|
||||
});
|
||||
return () => listener.remove();
|
||||
}, [updateValue, value, channelId, rootId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== lastNativeValue.current) {
|
||||
// May change when we implement Fabric
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ type Props = {
|
|||
canDelete: boolean;
|
||||
currentUserId?: string;
|
||||
currentUsername?: string;
|
||||
location: string;
|
||||
post: Post;
|
||||
showJoinLeave: boolean;
|
||||
testID?: string;
|
||||
|
|
@ -58,7 +59,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
});
|
||||
|
||||
const CombinedUserActivity = ({
|
||||
canDelete, currentUserId, currentUsername,
|
||||
canDelete, currentUserId, currentUsername, location,
|
||||
post, showJoinLeave, testID, theme, usernamesById = {}, style,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
|
|
@ -135,7 +136,9 @@ const CombinedUserActivity = ({
|
|||
return (
|
||||
<LastUsers
|
||||
key={postType + actorId}
|
||||
channelId={post.channel_id}
|
||||
actor={actor}
|
||||
location={location}
|
||||
postType={postType}
|
||||
theme={theme}
|
||||
usernames={usernames}
|
||||
|
|
@ -162,8 +165,10 @@ const CombinedUserActivity = ({
|
|||
const formattedMessage = intl.formatMessage(localeHolder, {firstUser, secondUser, actor});
|
||||
return (
|
||||
<Markdown
|
||||
channelId={post.channel_id}
|
||||
key={postType + actorId}
|
||||
baseTextStyle={styles.baseText}
|
||||
location={location}
|
||||
textStyles={textStyles}
|
||||
value={formattedMessage}
|
||||
theme={theme}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import {postTypeMessages, systemMessages} from './messages';
|
|||
|
||||
type LastUsersProps = {
|
||||
actor: string;
|
||||
channelId?: string;
|
||||
location: string;
|
||||
postType: string;
|
||||
usernames: string[];
|
||||
theme: Theme;
|
||||
|
|
@ -35,7 +37,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
const LastUsers = ({actor, postType, theme, usernames}: LastUsersProps) => {
|
||||
const LastUsers = ({actor, channelId, location, postType, theme, usernames}: LastUsersProps) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const intl = useIntl();
|
||||
const style = getStyleSheet(theme);
|
||||
|
|
@ -58,6 +60,8 @@ const LastUsers = ({actor, postType, theme, usernames}: LastUsersProps) => {
|
|||
return (
|
||||
<Markdown
|
||||
baseTextStyle={style.baseText}
|
||||
channelId={channelId}
|
||||
location={location}
|
||||
textStyles={textStyles}
|
||||
value={formattedMessage}
|
||||
theme={theme}
|
||||
|
|
@ -71,8 +75,10 @@ const LastUsers = ({actor, postType, theme, usernames}: LastUsersProps) => {
|
|||
return (
|
||||
<Text>
|
||||
<FormattedMarkdownText
|
||||
channelId={channelId}
|
||||
id={'last_users_message.first'}
|
||||
defaultMessage={'{firstUser} and '}
|
||||
location={location}
|
||||
values={{firstUser}}
|
||||
baseTextStyle={style.baseText}
|
||||
style={style.baseText}
|
||||
|
|
@ -90,8 +96,10 @@ const LastUsers = ({actor, postType, theme, usernames}: LastUsersProps) => {
|
|||
/>
|
||||
</Text>
|
||||
<FormattedMarkdownText
|
||||
channelId={channelId}
|
||||
id={systemMessages[postType].id}
|
||||
defaultMessage={systemMessages[postType].defaultMessage}
|
||||
location={location}
|
||||
values={{actor}}
|
||||
baseTextStyle={style.baseText}
|
||||
style={style.baseText}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,29 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {ReactNode, useRef} from 'react';
|
||||
import React, {ReactNode} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Keyboard, Platform, StyleSheet, TouchableOpacity, View} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
import {View as ViewConstant} from '@constants';
|
||||
import {Screens, View as ViewConstant} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {showModal} from '@screens/navigation';
|
||||
import {openAsBottomSheet} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
import type {ImageSource} from 'react-native-vector-icons/Icon';
|
||||
|
||||
type AvatarProps = {
|
||||
author: UserModel;
|
||||
enablePostIconOverride?: boolean;
|
||||
isAutoReponse: boolean;
|
||||
location: string;
|
||||
post: PostModel;
|
||||
}
|
||||
|
||||
|
|
@ -33,8 +33,7 @@ const style = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
const Avatar = ({author, enablePostIconOverride, isAutoReponse, post}: AvatarProps) => {
|
||||
const closeButton = useRef<ImageSource>();
|
||||
const Avatar = ({author, enablePostIconOverride, isAutoReponse, location, post}: AvatarProps) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
|
|
@ -91,27 +90,21 @@ const Avatar = ({author, enablePostIconOverride, isAutoReponse, post}: AvatarPro
|
|||
);
|
||||
}
|
||||
|
||||
const onViewUserProfile = preventDoubleTap(() => {
|
||||
const screen = 'UserProfile';
|
||||
const openUserProfile = preventDoubleTap(() => {
|
||||
const screen = Screens.USER_PROFILE;
|
||||
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
|
||||
const passProps = {author};
|
||||
|
||||
if (!closeButton.current) {
|
||||
closeButton.current = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
}
|
||||
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: 'close-settings',
|
||||
icon: closeButton.current,
|
||||
testID: 'close.settings.button',
|
||||
}],
|
||||
},
|
||||
const closeButtonId = 'close-user-profile';
|
||||
const props = {
|
||||
closeButtonId,
|
||||
userId: author.id,
|
||||
channelId: post.channelId,
|
||||
location,
|
||||
userIconOverride: post.props?.override_username,
|
||||
usernameOverride: post.props?.override_icon_url,
|
||||
};
|
||||
|
||||
Keyboard.dismiss();
|
||||
showModal(screen, title, passProps, options);
|
||||
openAsBottomSheet({screen, title, theme, closeButtonId, props});
|
||||
});
|
||||
|
||||
let component = (
|
||||
|
|
@ -126,7 +119,7 @@ const Avatar = ({author, enablePostIconOverride, isAutoReponse, post}: AvatarPro
|
|||
|
||||
if (!fromWebHook) {
|
||||
component = (
|
||||
<TouchableOpacity onPress={onViewUserProfile}>
|
||||
<TouchableOpacity onPress={openUserProfile}>
|
||||
{component}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import type UserModel from '@typings/database/models/servers/user';
|
|||
type AddMembersProps = {
|
||||
channelType: string | null;
|
||||
currentUser: UserModel;
|
||||
location: string;
|
||||
post: PostModel;
|
||||
theme: Theme;
|
||||
}
|
||||
|
|
@ -35,7 +36,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
const AddMembers = ({channelType, currentUser, post, theme}: AddMembersProps) => {
|
||||
const AddMembers = ({channelType, currentUser, location, post, theme}: AddMembersProps) => {
|
||||
const intl = useIntl();
|
||||
const styles = getStyleSheet(theme);
|
||||
const textStyles = getMarkdownTextStyles(theme);
|
||||
|
|
@ -83,6 +84,8 @@ const AddMembers = ({channelType, currentUser, post, theme}: AddMembersProps) =>
|
|||
if (names.length === 1) {
|
||||
return (
|
||||
<AtMention
|
||||
channelId={post.channelId}
|
||||
location={location}
|
||||
mentionName={names[0]}
|
||||
mentionStyle={textStyles.mention}
|
||||
/>
|
||||
|
|
@ -110,6 +113,8 @@ const AddMembers = ({channelType, currentUser, post, theme}: AddMembersProps) =>
|
|||
return (
|
||||
<AtMention
|
||||
key={username}
|
||||
channelId={post.channelId}
|
||||
location={location}
|
||||
mentionStyle={textStyles.mention}
|
||||
mentionName={username}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
|
|||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
location: string;
|
||||
theme: Theme;
|
||||
value: string;
|
||||
}
|
||||
|
|
@ -26,7 +28,7 @@ const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
},
|
||||
}));
|
||||
|
||||
const EmbedText = ({theme, value}: Props) => {
|
||||
const EmbedText = ({channelId, location, theme, value}: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [height, setHeight] = useState<number|undefined>();
|
||||
const dimensions = useWindowDimensions();
|
||||
|
|
@ -51,6 +53,8 @@ const EmbedText = ({theme, value}: Props) => {
|
|||
<View onLayout={onLayout}>
|
||||
<Markdown
|
||||
baseTextStyle={style.message}
|
||||
channelId={channelId}
|
||||
location={location}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
disableGallery={true}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import Markdown from '@components/markdown';
|
|||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
location: string;
|
||||
theme: Theme;
|
||||
value: string;
|
||||
}
|
||||
|
|
@ -32,16 +34,18 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
const EmbedTitle = ({theme, value}: Props) => {
|
||||
const EmbedTitle = ({channelId, location, theme, value}: Props) => {
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<Markdown
|
||||
channelId={channelId}
|
||||
disableHashtags={true}
|
||||
disableAtMentions={true}
|
||||
disableChannelLink={true}
|
||||
disableGallery={true}
|
||||
location={location}
|
||||
autolinkedUrlSchemes={[]}
|
||||
mentionKeys={[]}
|
||||
theme={theme}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import type PostModel from '@typings/database/models/servers/post';
|
|||
|
||||
type Props = {
|
||||
embed: AppBinding;
|
||||
location: string;
|
||||
post: PostModel;
|
||||
theme: Theme;
|
||||
}
|
||||
|
|
@ -37,7 +38,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
const EmbeddedBinding = ({embed, post, theme}: Props) => {
|
||||
const EmbeddedBinding = ({embed, location, post, theme}: Props) => {
|
||||
const style = getStyleSheet(theme);
|
||||
const [cleanedBindings, setCleanedBindings] = useState<AppBinding[]>([]);
|
||||
|
||||
|
|
@ -52,12 +53,16 @@ const EmbeddedBinding = ({embed, post, theme}: Props) => {
|
|||
<View style={style.container}>
|
||||
{Boolean(embed.label) &&
|
||||
<EmbedTitle
|
||||
channelId={post.channelId}
|
||||
location={location}
|
||||
theme={theme}
|
||||
value={embed.label}
|
||||
/>
|
||||
}
|
||||
{Boolean(embed.description) &&
|
||||
<EmbedText
|
||||
channelId={post.channelId}
|
||||
location={location}
|
||||
value={embed.description!}
|
||||
theme={theme}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@ import EmbeddedBinding from './embedded_binding';
|
|||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
type Props = {
|
||||
location: string;
|
||||
post: PostModel;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const EmbeddedBindings = ({post, theme}: Props) => {
|
||||
const EmbeddedBindings = ({location, post, theme}: Props) => {
|
||||
const content: React.ReactNode[] = [];
|
||||
const embeds: AppBinding[] = post.props.app_bindings;
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ const EmbeddedBindings = ({post, theme}: Props) => {
|
|||
content.push(
|
||||
<EmbeddedBinding
|
||||
embed={embed}
|
||||
location={location}
|
||||
key={'binding_' + i.toString()}
|
||||
post={post}
|
||||
theme={theme}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ const Content = ({isReplyPost, layoutWidth, location, post, theme}: ContentProps
|
|||
return (
|
||||
<MessageAttachments
|
||||
attachments={post.props.attachments}
|
||||
channelId={post.channelId}
|
||||
layoutWidth={layoutWidth}
|
||||
location={location}
|
||||
metadata={post.metadata!}
|
||||
|
|
@ -91,6 +92,7 @@ const Content = ({isReplyPost, layoutWidth, location, post, theme}: ContentProps
|
|||
if (post.props.app_bindings?.length) {
|
||||
return (
|
||||
<EmbeddedBindings
|
||||
location={location}
|
||||
post={post}
|
||||
theme={theme}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ import type {MarkdownBlockStyles, MarkdownTextStyles} from '@typings/global/mark
|
|||
type Props = {
|
||||
baseTextStyle: StyleProp<TextStyle>;
|
||||
blockStyles?: MarkdownBlockStyles;
|
||||
channelId: string;
|
||||
fields: MessageAttachmentField[];
|
||||
location: string;
|
||||
metadata?: PostMetadata;
|
||||
textStyles?: MarkdownTextStyles;
|
||||
theme: Theme;
|
||||
|
|
@ -44,7 +46,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
const AttachmentFields = ({baseTextStyle, blockStyles, fields, metadata, textStyles, theme}: Props) => {
|
||||
const AttachmentFields = ({baseTextStyle, blockStyles, channelId, fields, location, metadata, textStyles, theme}: Props) => {
|
||||
const style = getStyleSheet(theme);
|
||||
const fieldTables = [];
|
||||
|
||||
|
|
@ -92,10 +94,12 @@ const AttachmentFields = ({baseTextStyle, blockStyles, fields, metadata, textSty
|
|||
>
|
||||
<Markdown
|
||||
baseTextStyle={baseTextStyle}
|
||||
channelId={channelId}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
disableGallery={true}
|
||||
imagesMetadata={metadata?.images}
|
||||
location={location}
|
||||
theme={theme}
|
||||
value={(field.value || '')}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import type {MarkdownBlockStyles, MarkdownTextStyles} from '@typings/global/mark
|
|||
type Props = {
|
||||
baseTextStyle: StyleProp<TextStyle>;
|
||||
blockStyles?: MarkdownBlockStyles;
|
||||
channelId: string;
|
||||
location: string;
|
||||
metadata?: PostMetadata;
|
||||
textStyles?: MarkdownTextStyles;
|
||||
theme: Theme;
|
||||
|
|
@ -40,10 +42,12 @@ export default function AttachmentPreText(props: Props) {
|
|||
<View style={style.container}>
|
||||
<Markdown
|
||||
baseTextStyle={baseTextStyle}
|
||||
channelId={props.channelId}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
disableGallery={true}
|
||||
imagesMetadata={metadata?.images}
|
||||
location={props.location}
|
||||
theme={props.theme}
|
||||
value={value}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ import type {MarkdownBlockStyles, MarkdownTextStyles} from '@typings/global/mark
|
|||
type Props = {
|
||||
baseTextStyle: StyleProp<TextStyle>;
|
||||
blockStyles?: MarkdownBlockStyles;
|
||||
channelId: string;
|
||||
hasThumbnail?: boolean;
|
||||
location: string;
|
||||
metadata?: PostMetadata;
|
||||
textStyles?: MarkdownTextStyles;
|
||||
theme: Theme;
|
||||
|
|
@ -28,7 +30,7 @@ const style = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
const AttachmentText = ({baseTextStyle, blockStyles, hasThumbnail, metadata, textStyles, theme, value}: Props) => {
|
||||
const AttachmentText = ({baseTextStyle, blockStyles, channelId, hasThumbnail, location, metadata, textStyles, theme, value}: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [height, setHeight] = useState<number|undefined>();
|
||||
const dimensions = useWindowDimensions();
|
||||
|
|
@ -49,6 +51,8 @@ const AttachmentText = ({baseTextStyle, blockStyles, hasThumbnail, metadata, tex
|
|||
>
|
||||
<View onLayout={onLayout}>
|
||||
<Markdown
|
||||
channelId={channelId}
|
||||
location={location}
|
||||
baseTextStyle={baseTextStyle}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ import {makeStyleSheetFromTheme} from '@utils/theme';
|
|||
import {tryOpenURL} from '@utils/url';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
link?: string;
|
||||
location: string;
|
||||
theme: Theme;
|
||||
value?: string;
|
||||
}
|
||||
|
|
@ -33,7 +35,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
const AttachmentTitle = ({link, theme, value}: Props) => {
|
||||
const AttachmentTitle = ({channelId, link, location, theme, value}: Props) => {
|
||||
const intl = useIntl();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
|
|
@ -69,6 +71,8 @@ const AttachmentTitle = ({link, theme, value}: Props) => {
|
|||
} else {
|
||||
title = (
|
||||
<Markdown
|
||||
channelId={channelId}
|
||||
location={location}
|
||||
isEdited={false}
|
||||
isReplyPost={false}
|
||||
disableHashtags={true}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import MessageAttachment from './message_attachment';
|
|||
|
||||
type Props = {
|
||||
attachments: MessageAttachment[];
|
||||
channelId: string;
|
||||
layoutWidth?: number;
|
||||
location: string;
|
||||
metadata?: PostMetadata;
|
||||
|
|
@ -22,13 +23,14 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
const MessageAttachments = ({attachments, layoutWidth, location, metadata, postId, theme}: Props) => {
|
||||
const MessageAttachments = ({attachments, channelId, layoutWidth, location, metadata, postId, theme}: Props) => {
|
||||
const content: React.ReactNode[] = [];
|
||||
|
||||
attachments.forEach((attachment, i) => {
|
||||
content.push(
|
||||
<MessageAttachment
|
||||
attachment={attachment}
|
||||
channelId={channelId}
|
||||
key={'att_' + i.toString()}
|
||||
layoutWidth={layoutWidth}
|
||||
location={location}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import AttachmentTitle from './attachment_title';
|
|||
|
||||
type Props = {
|
||||
attachment: MessageAttachment;
|
||||
channelId: string;
|
||||
layoutWidth?: number;
|
||||
location: string;
|
||||
metadata?: PostMetadata;
|
||||
|
|
@ -52,7 +53,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
export default function MessageAttachment({attachment, layoutWidth, location, metadata, postId, theme}: Props) {
|
||||
export default function MessageAttachment({attachment, channelId, layoutWidth, location, metadata, postId, theme}: Props) {
|
||||
const style = getStyleSheet(theme);
|
||||
const blockStyles = getMarkdownBlockStyles(theme);
|
||||
const textStyles = getMarkdownTextStyles(theme);
|
||||
|
|
@ -71,6 +72,8 @@ export default function MessageAttachment({attachment, layoutWidth, location, me
|
|||
<AttachmentPreText
|
||||
baseTextStyle={style.message}
|
||||
blockStyles={blockStyles}
|
||||
channelId={channelId}
|
||||
location={location}
|
||||
metadata={metadata}
|
||||
textStyles={textStyles}
|
||||
theme={theme}
|
||||
|
|
@ -87,6 +90,8 @@ export default function MessageAttachment({attachment, layoutWidth, location, me
|
|||
}
|
||||
{Boolean(attachment.title) &&
|
||||
<AttachmentTitle
|
||||
channelId={channelId}
|
||||
location={location}
|
||||
link={attachment.title_link}
|
||||
theme={theme}
|
||||
value={attachment.title}
|
||||
|
|
@ -99,6 +104,8 @@ export default function MessageAttachment({attachment, layoutWidth, location, me
|
|||
<AttachmentText
|
||||
baseTextStyle={style.message}
|
||||
blockStyles={blockStyles}
|
||||
channelId={channelId}
|
||||
location={location}
|
||||
hasThumbnail={Boolean(attachment.thumb_url)}
|
||||
metadata={metadata}
|
||||
textStyles={textStyles}
|
||||
|
|
@ -110,6 +117,8 @@ export default function MessageAttachment({attachment, layoutWidth, location, me
|
|||
<AttachmentFields
|
||||
baseTextStyle={style.message}
|
||||
blockStyles={blockStyles}
|
||||
channelId={channelId}
|
||||
location={location}
|
||||
fields={attachment.fields}
|
||||
metadata={metadata}
|
||||
textStyles={textStyles}
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ const Body = ({
|
|||
} else if (isPostAddChannelMember) {
|
||||
message = (
|
||||
<AddMembers
|
||||
location={location}
|
||||
post={post}
|
||||
theme={theme}
|
||||
/>
|
||||
|
|
@ -180,6 +181,7 @@ const Body = ({
|
|||
}
|
||||
{hasReactions && showAddReaction &&
|
||||
<Reactions
|
||||
location={location}
|
||||
post={post}
|
||||
theme={theme}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ const Message = ({currentUser, highlight, isEdited, isPendingOrFailed, isReplyPo
|
|||
<Markdown
|
||||
baseTextStyle={style.message}
|
||||
blockStyles={blockStyles}
|
||||
channelId={post.channelId}
|
||||
channelMentions={post.props?.channel_mentions}
|
||||
imagesMetadata={post.metadata?.images}
|
||||
isEdited={isEdited}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ type ReactionsProps = {
|
|||
canRemoveReaction: boolean;
|
||||
disabled: boolean;
|
||||
currentUserId: string;
|
||||
location: string;
|
||||
postId: string;
|
||||
reactions: ReactionModel[];
|
||||
theme: Theme;
|
||||
|
|
@ -59,7 +60,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled, postId, reactions, theme}: ReactionsProps) => {
|
||||
const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled, location, postId, reactions, theme}: ReactionsProps) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const isTablet = useIsTablet();
|
||||
|
|
@ -137,6 +138,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
|
|||
const screen = Screens.REACTIONS;
|
||||
const passProps = {
|
||||
initialEmoji,
|
||||
location,
|
||||
postId,
|
||||
};
|
||||
|
||||
|
|
@ -150,7 +152,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
|
|||
showModalOverCurrentContext(screen, passProps, bottomSheetModalOptions(theme));
|
||||
}
|
||||
}
|
||||
}, [intl, isTablet, postId, theme]);
|
||||
}, [intl, isTablet, location, postId, theme]);
|
||||
|
||||
let addMoreReactions = null;
|
||||
const {reactionsByName, highlightedReactions} = buildReactionsMap();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import type ThreadModel from '@typings/database/models/servers/thread';
|
|||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
location: string;
|
||||
participants: UserModel[];
|
||||
teamId?: string;
|
||||
thread: ThreadModel;
|
||||
|
|
@ -76,7 +78,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
const Footer = ({participants, teamId, thread}: Props) => {
|
||||
const Footer = ({channelId, location, participants, teamId, thread}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
|
@ -156,6 +158,8 @@ const Footer = ({participants, teamId, thread}: Props) => {
|
|||
if (participantsList.length) {
|
||||
userAvatarsStack = (
|
||||
<UserAvatarsStack
|
||||
channelId={channelId}
|
||||
location={location}
|
||||
style={styles.avatarsContainer}
|
||||
users={participantsList}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,27 +1,28 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useRef} from 'react';
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Keyboard, Text, TouchableOpacity, useWindowDimensions, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {showModal} from '@screens/navigation';
|
||||
import {Screens} from '@constants';
|
||||
import {openAsBottomSheet} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import type {ImageSource} from 'react-native-vector-icons/Icon';
|
||||
|
||||
type HeaderDisplayNameProps = {
|
||||
channelId: string;
|
||||
commentCount: number;
|
||||
displayName?: string;
|
||||
isAutomation: boolean;
|
||||
location: string;
|
||||
rootPostAuthor?: string;
|
||||
shouldRenderReplyButton?: boolean;
|
||||
theme: Theme;
|
||||
userIconOverride?: string;
|
||||
userId: string;
|
||||
usernameOverride?: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
|
|
@ -49,47 +50,35 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
});
|
||||
|
||||
const HeaderDisplayName = ({
|
||||
commentCount, displayName, isAutomation, rootPostAuthor, shouldRenderReplyButton, theme, userId,
|
||||
channelId, commentCount, displayName,
|
||||
location, rootPostAuthor,
|
||||
shouldRenderReplyButton, theme,
|
||||
userIconOverride, userId, usernameOverride,
|
||||
}: HeaderDisplayNameProps) => {
|
||||
const closeButton = useRef<ImageSource>();
|
||||
const dimensions = useWindowDimensions();
|
||||
const intl = useIntl();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const onPress = useCallback(preventDoubleTap(() => {
|
||||
const screen = 'UserProfile';
|
||||
const screen = Screens.USER_PROFILE;
|
||||
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
|
||||
const passProps = {userId};
|
||||
|
||||
if (!closeButton.current) {
|
||||
closeButton.current = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
}
|
||||
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: 'close-user-profile',
|
||||
icon: closeButton.current,
|
||||
testID: 'close.user_profile.button',
|
||||
}],
|
||||
},
|
||||
};
|
||||
const closeButtonId = 'close-user-profile';
|
||||
const props = {closeButtonId, userId, channelId, location, userIconOverride, usernameOverride};
|
||||
|
||||
Keyboard.dismiss();
|
||||
showModal(screen, title, passProps, options);
|
||||
}), []);
|
||||
openAsBottomSheet({screen, title, theme, closeButtonId, props});
|
||||
}), [intl.locale, channelId, userIconOverride, userId, usernameOverride, theme]);
|
||||
|
||||
const calcNameWidth = () => {
|
||||
const isLandscape = dimensions.width > dimensions.height;
|
||||
|
||||
const showReply = shouldRenderReplyButton || (!rootPostAuthor && commentCount > 0);
|
||||
const reduceWidth = showReply && isAutomation;
|
||||
|
||||
if (reduceWidth && isLandscape) {
|
||||
if (showReply && isLandscape) {
|
||||
return style.displayNameContainerLandscapeBotReplyWidth;
|
||||
} else if (isLandscape) {
|
||||
return style.displayNameContainerLandscape;
|
||||
} else if (reduceWidth) {
|
||||
} else if (showReply) {
|
||||
return style.displayNameContainerBotReplyWidth;
|
||||
}
|
||||
return undefined;
|
||||
|
|
@ -98,20 +87,7 @@ const HeaderDisplayName = ({
|
|||
const displayNameWidth = calcNameWidth();
|
||||
const displayNameStyle = [style.displayNameContainer, displayNameWidth];
|
||||
|
||||
if (isAutomation) {
|
||||
return (
|
||||
<View style={displayNameStyle}>
|
||||
<Text
|
||||
style={style.displayName}
|
||||
ellipsizeMode={'tail'}
|
||||
numberOfLines={1}
|
||||
testID='post_header.display_name'
|
||||
>
|
||||
{displayName}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
} else if (displayName) {
|
||||
if (displayName) {
|
||||
return (
|
||||
<View style={displayNameStyle}>
|
||||
<TouchableOpacity onPress={onPress}>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ type HeaderProps = {
|
|||
enablePostUsernameOverride: boolean;
|
||||
isAutoResponse: boolean;
|
||||
isCRTEnabled?: boolean;
|
||||
isCustomStatusEnabled: boolean;
|
||||
isEphemeral: boolean;
|
||||
isMilitaryTime: boolean;
|
||||
isPendingOrFailed: boolean;
|
||||
|
|
@ -72,7 +71,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
|
||||
const Header = (props: HeaderProps) => {
|
||||
const {
|
||||
author, commentCount = 0, currentUser, enablePostUsernameOverride, isAutoResponse, isCRTEnabled, isCustomStatusEnabled,
|
||||
author, commentCount = 0, currentUser, enablePostUsernameOverride, isAutoResponse, isCRTEnabled,
|
||||
isEphemeral, isMilitaryTime, isPendingOrFailed, isSystemPost, isTimezoneEnabled, isWebHook,
|
||||
location, post, rootPostAuthor, shouldRenderReplyButton, teammateNameDisplay,
|
||||
} = props;
|
||||
|
|
@ -86,9 +85,8 @@ const Header = (props: HeaderProps) => {
|
|||
const customStatus = getUserCustomStatus(author);
|
||||
const customStatusExpired = isCustomStatusExpired(author);
|
||||
const showCustomStatusEmoji = Boolean(
|
||||
isCustomStatusEnabled && displayName &&
|
||||
!(isSystemPost || author.isBot || isAutoResponse || isWebHook) &&
|
||||
customStatus,
|
||||
displayName && customStatus &&
|
||||
!(isSystemPost || author.isBot || isAutoResponse || isWebHook),
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -96,13 +94,16 @@ const Header = (props: HeaderProps) => {
|
|||
<View style={[style.container, pendingPostStyle]}>
|
||||
<View style={style.wrapper}>
|
||||
<HeaderDisplayName
|
||||
channelId={post.channelId}
|
||||
commentCount={commentCount}
|
||||
displayName={displayName}
|
||||
isAutomation={author.isBot || isAutoResponse || isWebHook}
|
||||
location={location}
|
||||
rootPostAuthor={rootAuthorDisplayName}
|
||||
shouldRenderReplyButton={shouldRenderReplyButton}
|
||||
theme={theme}
|
||||
userIconOverride={post.props?.override_icon_url}
|
||||
userId={post.userId}
|
||||
usernameOverride={post.props?.override_username}
|
||||
/>
|
||||
{showCustomStatusEmoji && !customStatusExpired && Boolean(customStatus?.emoji) && (
|
||||
<CustomStatusEmoji
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@
|
|||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {combineLatest, of as of$} from 'rxjs';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {map, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import {getPreferenceAsBool, getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
import {getPreferenceAsBool} from '@helpers/api/preference';
|
||||
import {queryPostReplies} from '@queries/servers/post';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {observeConfig, observeLicense} from '@queries/servers/system';
|
||||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
import {observeConfigBooleanValue} from '@queries/servers/system';
|
||||
import {observeTeammateNameDisplay} from '@queries/servers/user';
|
||||
|
||||
import Header from './header';
|
||||
|
||||
|
|
@ -26,18 +26,13 @@ type HeaderInputProps = {
|
|||
const withHeaderProps = withObservables(
|
||||
['post', 'differentThreadSequence'],
|
||||
({post, database, differentThreadSequence}: WithDatabaseArgs & HeaderInputProps) => {
|
||||
const config = observeConfig(database);
|
||||
const license = observeLicense(database);
|
||||
const preferences = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).
|
||||
observeWithColumns(['value']);
|
||||
const author = post.author.observe();
|
||||
const enablePostUsernameOverride = config.pipe(map((cfg) => cfg?.EnablePostUsernameOverride === 'true'));
|
||||
const isTimezoneEnabled = config.pipe(map((cfg) => cfg?.ExperimentalTimezone === 'true'));
|
||||
const enablePostUsernameOverride = observeConfigBooleanValue(database, 'EnablePostUsernameOverride');
|
||||
const isTimezoneEnabled = observeConfigBooleanValue(database, 'ExperimentalTimezone');
|
||||
const isMilitaryTime = preferences.pipe(map((prefs) => getPreferenceAsBool(prefs, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', false)));
|
||||
const isCustomStatusEnabled = config.pipe(map((cfg) => cfg?.EnableCustomUserStatuses === 'true' && isMinimumServerVersion(cfg.Version, 5, 36)));
|
||||
const teammateNameDisplay = combineLatest([preferences, config, license]).pipe(
|
||||
map(([prefs, cfg, lcs]) => getTeammateNameDisplaySetting(prefs, cfg, lcs)),
|
||||
);
|
||||
const teammateNameDisplay = observeTeammateNameDisplay(database);
|
||||
const commentCount = queryPostReplies(database, post.rootId || post.id).observeCount();
|
||||
const rootPostAuthor = differentThreadSequence ? post.root.observe().pipe(switchMap((root) => {
|
||||
if (root.length) {
|
||||
|
|
@ -51,7 +46,6 @@ const withHeaderProps = withObservables(
|
|||
author,
|
||||
commentCount,
|
||||
enablePostUsernameOverride,
|
||||
isCustomStatusEnabled,
|
||||
isMilitaryTime,
|
||||
isTimezoneEnabled,
|
||||
rootPostAuthor,
|
||||
|
|
|
|||
|
|
@ -230,6 +230,7 @@ const Post = ({
|
|||
) : (
|
||||
<Avatar
|
||||
isAutoReponse={isAutoResponder}
|
||||
location={location}
|
||||
post={post}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -266,6 +267,7 @@ const Post = ({
|
|||
if (isSystemPost && !isEphemeral && !isAutoResponder) {
|
||||
body = (
|
||||
<SystemMessage
|
||||
location={location}
|
||||
post={post}
|
||||
/>
|
||||
);
|
||||
|
|
@ -297,7 +299,11 @@ const Post = ({
|
|||
if (isCRTEnabled && thread) {
|
||||
if (thread.replyCount > 0 || thread.isFollowing) {
|
||||
footer = (
|
||||
<Footer thread={thread}/>
|
||||
<Footer
|
||||
channelId={post.channelId}
|
||||
location={location}
|
||||
thread={thread}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (thread.unreadMentions || thread.unreadReplies) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import type {PrimitiveType} from 'intl-messageformat';
|
|||
|
||||
type SystemMessageProps = {
|
||||
author?: UserModel;
|
||||
location: string;
|
||||
post: PostModel;
|
||||
}
|
||||
|
||||
|
|
@ -64,7 +65,7 @@ const renderUsername = (value = '') => {
|
|||
return value;
|
||||
};
|
||||
|
||||
const renderMessage = ({styles, intl, localeHolder, theme, values, skipMarkdown = false}: RenderMessageProps) => {
|
||||
const renderMessage = ({location, post, styles, intl, localeHolder, theme, values, skipMarkdown = false}: RenderMessageProps) => {
|
||||
const {containerStyle, messageStyle, textStyles} = styles;
|
||||
|
||||
if (skipMarkdown) {
|
||||
|
|
@ -79,7 +80,9 @@ const renderMessage = ({styles, intl, localeHolder, theme, values, skipMarkdown
|
|||
<View style={containerStyle}>
|
||||
<Markdown
|
||||
baseTextStyle={messageStyle}
|
||||
channelId={post.channelId}
|
||||
disableGallery={true}
|
||||
location={location}
|
||||
textStyles={textStyles}
|
||||
value={intl.formatMessage(localeHolder, values)}
|
||||
theme={theme}
|
||||
|
|
@ -88,7 +91,7 @@ const renderMessage = ({styles, intl, localeHolder, theme, values, skipMarkdown
|
|||
);
|
||||
};
|
||||
|
||||
const renderHeaderChangeMessage = ({post, author, styles, intl, theme}: RenderersProps) => {
|
||||
const renderHeaderChangeMessage = ({post, author, location, styles, intl, theme}: RenderersProps) => {
|
||||
let values;
|
||||
|
||||
if (!author?.username) {
|
||||
|
|
@ -108,7 +111,7 @@ const renderHeaderChangeMessage = ({post, author, styles, intl, theme}: Renderer
|
|||
};
|
||||
|
||||
values = {username, oldHeader, newHeader};
|
||||
return renderMessage({post, styles, intl, localeHolder, values, theme});
|
||||
return renderMessage({post, styles, intl, location, localeHolder, values, theme});
|
||||
}
|
||||
|
||||
localeHolder = {
|
||||
|
|
@ -117,7 +120,7 @@ const renderHeaderChangeMessage = ({post, author, styles, intl, theme}: Renderer
|
|||
};
|
||||
|
||||
values = {username, oldHeader, newHeader};
|
||||
return renderMessage({post, styles, intl, localeHolder, values, theme});
|
||||
return renderMessage({post, styles, intl, location, localeHolder, values, theme});
|
||||
} else if (post.props?.old_header) {
|
||||
localeHolder = {
|
||||
id: t('mobile.system_message.update_channel_header_message_and_forget.removed'),
|
||||
|
|
@ -125,13 +128,13 @@ const renderHeaderChangeMessage = ({post, author, styles, intl, theme}: Renderer
|
|||
};
|
||||
|
||||
values = {username, oldHeader, newHeader};
|
||||
return renderMessage({post, styles, intl, localeHolder, values, theme});
|
||||
return renderMessage({post, styles, intl, location, localeHolder, values, theme});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderPurposeChangeMessage = ({post, author, styles, intl, theme}: RenderersProps) => {
|
||||
const renderPurposeChangeMessage = ({post, author, location, styles, intl, theme}: RenderersProps) => {
|
||||
let values;
|
||||
|
||||
if (!author?.username) {
|
||||
|
|
@ -151,7 +154,7 @@ const renderPurposeChangeMessage = ({post, author, styles, intl, theme}: Rendere
|
|||
};
|
||||
|
||||
values = {username, oldPurpose, newPurpose};
|
||||
return renderMessage({post, styles, intl, localeHolder, values, skipMarkdown: true, theme});
|
||||
return renderMessage({post, styles, intl, location, localeHolder, values, skipMarkdown: true, theme});
|
||||
}
|
||||
|
||||
localeHolder = {
|
||||
|
|
@ -160,7 +163,7 @@ const renderPurposeChangeMessage = ({post, author, styles, intl, theme}: Rendere
|
|||
};
|
||||
|
||||
values = {username, oldPurpose, newPurpose};
|
||||
return renderMessage({post, styles, intl, localeHolder, values, skipMarkdown: true, theme});
|
||||
return renderMessage({post, styles, intl, location, localeHolder, values, skipMarkdown: true, theme});
|
||||
} else if (post.props?.old_purpose) {
|
||||
localeHolder = {
|
||||
id: t('mobile.system_message.update_channel_purpose_message.removed'),
|
||||
|
|
@ -168,13 +171,13 @@ const renderPurposeChangeMessage = ({post, author, styles, intl, theme}: Rendere
|
|||
};
|
||||
|
||||
values = {username, oldPurpose, newPurpose};
|
||||
return renderMessage({post, styles, intl, localeHolder, values, skipMarkdown: true, theme});
|
||||
return renderMessage({post, styles, intl, location, localeHolder, values, skipMarkdown: true, theme});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderDisplayNameChangeMessage = ({post, author, styles, intl, theme}: RenderersProps) => {
|
||||
const renderDisplayNameChangeMessage = ({post, author, location, styles, intl, theme}: RenderersProps) => {
|
||||
const oldDisplayName = post.props?.old_displayname;
|
||||
const newDisplayName = post.props?.new_displayname;
|
||||
|
||||
|
|
@ -189,10 +192,10 @@ const renderDisplayNameChangeMessage = ({post, author, styles, intl, theme}: Ren
|
|||
};
|
||||
|
||||
const values = {username, oldDisplayName, newDisplayName};
|
||||
return renderMessage({post, styles, intl, localeHolder, values, theme});
|
||||
return renderMessage({post, styles, intl, location, localeHolder, values, theme});
|
||||
};
|
||||
|
||||
const renderArchivedMessage = ({post, author, styles, intl, theme}: RenderersProps) => {
|
||||
const renderArchivedMessage = ({post, author, location, styles, intl, theme}: RenderersProps) => {
|
||||
const username = renderUsername(author?.username);
|
||||
const localeHolder = {
|
||||
id: t('mobile.system_message.channel_archived_message'),
|
||||
|
|
@ -200,10 +203,10 @@ const renderArchivedMessage = ({post, author, styles, intl, theme}: RenderersPro
|
|||
};
|
||||
|
||||
const values = {username};
|
||||
return renderMessage({post, styles, intl, localeHolder, values, theme});
|
||||
return renderMessage({post, styles, intl, location, localeHolder, values, theme});
|
||||
};
|
||||
|
||||
const renderUnarchivedMessage = ({post, author, styles, intl, theme}: RenderersProps) => {
|
||||
const renderUnarchivedMessage = ({post, author, location, styles, intl, theme}: RenderersProps) => {
|
||||
if (!author?.username) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -215,10 +218,10 @@ const renderUnarchivedMessage = ({post, author, styles, intl, theme}: RenderersP
|
|||
};
|
||||
|
||||
const values = {username};
|
||||
return renderMessage({post, styles, intl, localeHolder, values, theme});
|
||||
return renderMessage({post, styles, intl, location, localeHolder, values, theme});
|
||||
};
|
||||
|
||||
const renderAddGuestToChannelMessage = ({post, styles, intl, theme}: RenderersProps) => {
|
||||
const renderAddGuestToChannelMessage = ({post, location, styles, intl, theme}: RenderersProps) => {
|
||||
if (!post.props.username || !post.props.addedUsername) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -232,10 +235,10 @@ const renderAddGuestToChannelMessage = ({post, styles, intl, theme}: RenderersPr
|
|||
};
|
||||
|
||||
const values = {username, addedUsername};
|
||||
return renderMessage({post, styles, intl, localeHolder, values, theme});
|
||||
return renderMessage({post, styles, intl, location, localeHolder, values, theme});
|
||||
};
|
||||
|
||||
const renderGuestJoinChannelMessage = ({post, styles, intl, theme}: RenderersProps) => {
|
||||
const renderGuestJoinChannelMessage = ({post, styles, location, intl, theme}: RenderersProps) => {
|
||||
if (!post.props.username) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -247,7 +250,7 @@ const renderGuestJoinChannelMessage = ({post, styles, intl, theme}: RenderersPro
|
|||
};
|
||||
|
||||
const values = {username};
|
||||
return renderMessage({post, styles, intl, localeHolder, values, theme});
|
||||
return renderMessage({post, styles, intl, location, localeHolder, values, theme});
|
||||
};
|
||||
|
||||
const systemMessageRenderers = {
|
||||
|
|
@ -260,7 +263,7 @@ const systemMessageRenderers = {
|
|||
[Post.POST_TYPES.ADD_GUEST_TO_CHANNEL]: renderAddGuestToChannelMessage,
|
||||
};
|
||||
|
||||
export const SystemMessage = ({post, author}: SystemMessageProps) => {
|
||||
export const SystemMessage = ({post, location, author}: SystemMessageProps) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
|
|
@ -272,6 +275,8 @@ export const SystemMessage = ({post, author}: SystemMessageProps) => {
|
|||
return (
|
||||
<Markdown
|
||||
baseTextStyle={styles.messageStyle}
|
||||
channelId={post.channelId}
|
||||
location={location}
|
||||
disableGallery={true}
|
||||
textStyles={styles.textStyles}
|
||||
value={post.message}
|
||||
|
|
@ -280,7 +285,7 @@ export const SystemMessage = ({post, author}: SystemMessageProps) => {
|
|||
);
|
||||
}
|
||||
|
||||
return renderer({post, author, styles, intl, theme});
|
||||
return renderer({post, author, location, styles, intl, theme});
|
||||
};
|
||||
|
||||
export default SystemMessage;
|
||||
|
|
|
|||
|
|
@ -248,6 +248,7 @@ const PostList = ({
|
|||
const postProps = {
|
||||
currentUsername,
|
||||
postId: item,
|
||||
location,
|
||||
style: Platform.OS === 'ios' ? styles.scale : styles.container,
|
||||
testID: `${testID}.combined_user_activity`,
|
||||
showJoinLeave: shouldShowJoinLeaveMessages,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ function Footer({
|
|||
<Markdown
|
||||
baseTextStyle={style.helpText}
|
||||
textStyles={textStyles}
|
||||
disableAtMentions={true}
|
||||
location=''
|
||||
blockStyles={blockStyles}
|
||||
value={disabledText}
|
||||
theme={theme}
|
||||
|
|
@ -66,6 +68,8 @@ function Footer({
|
|||
baseTextStyle={style.helpText}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
disableAtMentions={true}
|
||||
location=''
|
||||
value={helpText}
|
||||
theme={theme}
|
||||
/>
|
||||
|
|
@ -77,6 +81,8 @@ function Footer({
|
|||
baseTextStyle={style.errorText}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
disableAtMentions={true}
|
||||
location=''
|
||||
value={errorText}
|
||||
theme={theme}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ type Props = {
|
|||
itemBounds: TutorialItemBounds;
|
||||
itemBorderRadius?: number;
|
||||
onDismiss: () => void;
|
||||
onShow: () => void;
|
||||
onShow?: () => void;
|
||||
}
|
||||
|
||||
const TutorialHighlight = ({children, itemBounds, itemBorderRadius, onDismiss, onShow}: Props) => {
|
||||
|
|
|
|||
62
app/components/tutorial_highlight/long_press.tsx
Normal file
62
app/components/tutorial_highlight/long_press.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleProp, StyleSheet, Text, TextStyle, View, ViewStyle} from 'react-native';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import LongPressIllustration from './long_press_illustration.svg';
|
||||
|
||||
type Props = {
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
message: string;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
textStyles?: StyleProp<TextStyle>;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
view: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderRadius: 8,
|
||||
height: 161,
|
||||
padding: 16,
|
||||
width: 247,
|
||||
},
|
||||
text: {
|
||||
...typography('Heading', 200),
|
||||
color: theme.centerChannelColor,
|
||||
marginTop: 8,
|
||||
paddingHorizontal: 12,
|
||||
textAlign: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
const TutorialSwipeLeft = ({containerStyle, message, style, textStyles}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View
|
||||
pointerEvents='none'
|
||||
style={[styles.container, containerStyle]}
|
||||
>
|
||||
<View style={[styles.view, style]}>
|
||||
<LongPressIllustration/>
|
||||
<Text style={[styles.text, textStyles]}>
|
||||
{message}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default TutorialSwipeLeft;
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<svg width="69" height="79" viewBox="0 0 69 79" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="31" cy="22.5" r="8.5" fill="#3F4350" fill-opacity="0.24"/>
|
||||
<circle cx="31" cy="22" r="21" stroke="#3F4350" stroke-opacity="0.08" stroke-width="2"/>
|
||||
<circle cx="31" cy="22" r="14" stroke="#3F4350" stroke-opacity="0.16" stroke-width="2"/>
|
||||
<g clip-path="url(#clip0_510_17728)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.8817 59.3289C23.782 59.1198 23.6852 58.9036 23.5896 58.68C23.1622 57.7293 22.7481 56.7384 22.3549 55.7976L22.3548 55.7974C21.6009 53.9934 20.9241 52.3738 20.3781 51.5763C20.3725 51.5692 20.3668 51.5623 20.3613 51.5556C20.3558 51.5489 20.3504 51.5424 20.345 51.536C19.9446 51.0595 19.5411 50.5806 19.1348 50.1041C18.4711 49.3623 17.7767 48.6482 17.0534 47.9636L16.9464 47.6157C16.8366 47.4976 16.7087 47.3639 16.5684 47.2171C15.3219 45.9132 13.0949 43.5837 13.8637 42.1814C14.6752 40.7014 16.8849 40.089 17.9873 40.3238C19.0896 40.5586 20.784 41.8548 20.784 41.8548L24.2798 44.9169L26.2242 47.7033L26.2246 47.704C26.8231 47.0294 27.1529 46.6265 27.1529 46.6265C27.1529 46.6265 27.6633 34.8887 27.6327 32.189C27.602 29.4893 27.5918 21.0278 27.7654 20.3337C27.9389 19.6396 28.5054 18.0627 31.0775 18.4557C33.6496 18.8486 34.0885 20.8543 34.0885 20.8543L34.3487 24.937L34.7877 38.2773C34.7877 38.2773 35.1857 34.2609 36.2881 33.4699C37.0689 32.9085 39.0592 33.0157 40.1718 33.4699C41.514 34.0364 42.0396 38.2671 42.0396 38.2671C42.0396 38.2671 42.4224 36.093 42.9123 35.2612C43.6166 34.0721 46.0254 34.9193 46.8777 35.5572C47.5105 36.0369 48.1127 41.0025 48.1127 41.0025C48.1127 41.0025 48.0514 39.4715 48.7914 38.8183C49.3936 38.3079 51.2206 38.6499 51.731 39.1908C52.8997 40.4361 53.4049 43.9064 53.4049 43.9064L53.6652 46.0651V50.5153C53.8702 53.0229 53.7288 56.1421 52.9787 57.9734C52.7392 58.5581 52.5196 59.0372 52.3161 59.481C51.8822 60.4271 51.5219 61.213 51.1996 62.5207C51.1738 62.9302 50.9608 66.154 50.593 67.0759C50.2 68.0608 48.8936 69.4949 46.8012 70.2808C43.8644 71.417 40.7175 71.9078 37.5742 71.72C35.699 71.5932 31.0356 70.6611 30.5211 68.3298C30.3147 67.3942 29.7628 66.5331 29.1906 65.7816C27.7084 64.3506 24.9074 61.4856 23.9003 59.3577L23.8817 59.3289Z" fill="#FFBC1F"/>
|
||||
<path d="M36.8853 33.2097C36.8853 33.2097 35.5242 33.5579 36.2876 43.6064C36.5224 46.0713 34.9358 48.0759 35.0379 46.2387C35.2114 43.1766 34.6449 39.7369 34.8592 37.5629C35.3185 33.0311 36.8853 33.2097 36.8853 33.2097Z" fill="#CC8F00"/>
|
||||
<path d="M44.1322 34.6795C44.1322 34.6795 42.1269 34.4246 43.303 44.535C43.548 46.9999 43.7495 47.8259 43.0299 46.7746C41.9224 45.1517 41.9071 43.1716 41.9531 38.8235C41.999 34.2917 44.1322 34.6795 44.1322 34.6795Z" fill="#CC8F00"/>
|
||||
<path d="M49.701 38.5781C49.701 38.5781 48.3408 39.7446 49.2662 45.6491C49.8079 48.9711 50.2411 48.6577 50.088 48.8006C49.9349 48.9435 48.4294 47.9636 48.0772 42.5438C47.8185 40.4356 48.3702 39.2702 48.7002 38.9082C49.0302 38.5462 49.701 38.5781 49.701 38.5781Z" fill="#CC8F00"/>
|
||||
<path d="M30.1274 18.4205C30.1274 18.4205 28.5466 18.6497 28.6538 23.0029C28.8171 29.7037 29.1641 30.3569 29.0263 34.3784C28.8885 38.3999 29.6643 43.9319 27.9495 47.1879C26.8574 49.265 26.1123 47.6983 26.1123 47.6983C26.1123 47.6983 28.419 45.7845 27.4443 23.0131C27.1609 18.0862 30.1274 18.4205 30.1274 18.4205Z" fill="#CC8F00"/>
|
||||
<path d="M28.9839 23.4788C28.6222 22.5746 28.1701 19.3192 28.6222 18.9575C29.0743 18.5958 32.6435 18.7706 32.9626 19.2537C33.2344 19.6651 33.1435 22.4841 32.9626 23.4788C32.7818 24.4735 29.3938 24.5036 28.9839 23.4788Z" fill="#FFD470"/>
|
||||
<path d="M16.6094 46.5194C16.6094 46.5194 19.2376 48.9895 21.3096 50.8624C22.6201 52.047 24.8223 56.9738 24.8116 56.9107C25.0225 57.6341 26.6741 61.3371 28.6637 62.4778C32.9637 64.9432 31.5271 70.3587 38.1811 70.2808C43.4121 70.2196 49.7069 69.1128 51.2022 62.4778C51.2022 62.4778 50.9859 66.091 50.593 67.0759C50.2 68.0609 48.8936 69.4949 46.8012 70.2808C43.8644 71.417 40.7175 71.9078 37.5742 71.72C35.699 71.5932 31.0356 70.6612 30.5211 68.3298C30.3147 67.3943 29.7628 66.5332 29.1905 65.7816C27.7017 64.3442 24.8822 61.4598 23.8868 59.329C22.5089 56.3792 21.2484 52.8119 20.3502 51.5361C19.333 50.2723 18.2316 49.0788 17.0534 47.9637L16.6094 46.5194Z" fill="#CC8F00"/>
|
||||
<path d="M29.5988 21.7714C29.4541 20.6863 29.5988 18.7593 29.5988 18.7593C29.418 18.7593 28.7218 18.8235 28.5982 18.9717C28.4051 19.2034 28.4056 19.7928 28.4533 20.5999C28.5191 21.7135 28.7941 23.05 28.984 23.4788C29.2481 24.0751 30.4552 24.2327 30.9375 24.2327C30.4854 24.0518 29.7797 23.1278 29.5988 21.7714Z" fill="#F5AB00"/>
|
||||
<path d="M17.0483 47.9688C17.1749 46.78 16.9528 45.5799 16.4091 44.5152C15.8654 43.4504 15.0235 42.5668 13.9863 41.9723C13.578 41.7222 12.9275 42.7334 13.2796 43.1774C13.6318 43.6214 16.4091 47.1827 17.0483 47.9688Z" fill="#FFD470"/>
|
||||
<path d="M13.2812 42.4391L17.0774 46.9564C17.0774 46.9564 17.1131 47.2169 17.0986 47.5182C17.0841 47.8194 17.0485 47.9688 17.0485 47.9688C17.0485 47.9688 14.8593 45.3353 13.8282 43.886C13.2566 43.1256 13.0212 43.0897 13.2812 42.4391Z" fill="#F5AB00"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_510_17728">
|
||||
<rect width="40.9141" height="53.3511" fill="white" transform="translate(13.1597 18.4149)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
|
|
@ -21,6 +21,8 @@ import type UserModel from '@typings/database/models/servers/user';
|
|||
const OVERFLOW_DISPLAY_LIMIT = 99;
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
location: string;
|
||||
users: UserModel[];
|
||||
breakAt?: number;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
|
|
@ -93,7 +95,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
const UserAvatarsStack = ({breakAt = 3, style: baseContainerStyle, users}: Props) => {
|
||||
const UserAvatarsStack = ({breakAt = 3, channelId, location, style: baseContainerStyle, users}: Props) => {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const isTablet = useIsTablet();
|
||||
|
|
@ -110,18 +112,22 @@ const UserAvatarsStack = ({breakAt = 3, style: baseContainerStyle, users}: Props
|
|||
/>
|
||||
</View>
|
||||
)}
|
||||
<UsersList users={users}/>
|
||||
<UsersList
|
||||
channelId={channelId}
|
||||
location={location}
|
||||
users={users}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
bottomSheet({
|
||||
closeButtonId: 'close-set-user-status',
|
||||
renderContent,
|
||||
snapPoints: [(Math.min(14, users.length) + 3) * 40, 10],
|
||||
initialSnapIndex: 1,
|
||||
snapPoints: ['90%', '50%', 10],
|
||||
title: intl.formatMessage({id: 'mobile.participants.header', defaultMessage: 'Thread Participants'}),
|
||||
theme,
|
||||
});
|
||||
}), [isTablet, theme, users]);
|
||||
}), [isTablet, theme, users, channelId, location]);
|
||||
|
||||
const displayUsers = users.slice(0, breakAt);
|
||||
const overflowUsersCount = Math.min(users.length - displayUsers.length, OVERFLOW_DISPLAY_LIMIT);
|
||||
|
|
|
|||
|
|
@ -1,34 +1,107 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet} from 'react-native';
|
||||
import React, {useCallback, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Keyboard, NativeScrollEvent, NativeSyntheticEvent, PanResponder, StyleProp, StyleSheet, TouchableOpacity, ViewStyle} from 'react-native';
|
||||
import {FlatList} from 'react-native-gesture-handler';
|
||||
|
||||
import UserItem from '@components/user_item';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {dismissBottomSheet, openAsBottomSheet} from '@screens/navigation';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
location: string;
|
||||
users: UserModel[];
|
||||
};
|
||||
|
||||
type ItemProps = {
|
||||
channelId: string;
|
||||
containerStyle: StyleProp<ViewStyle>;
|
||||
location: string;
|
||||
user: UserModel;
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
paddingLeft: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const UsersList = ({users}: Props) => {
|
||||
const Item = ({channelId, containerStyle, location, user}: ItemProps) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const openUserProfile = async () => {
|
||||
if (user) {
|
||||
await dismissBottomSheet(Screens.BOTTOM_SHEET);
|
||||
const screen = Screens.USER_PROFILE;
|
||||
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
|
||||
const closeButtonId = 'close-user-profile';
|
||||
const props = {closeButtonId, location, userId: user.id, channelId};
|
||||
|
||||
Keyboard.dismiss();
|
||||
openAsBottomSheet({screen, title, theme, closeButtonId, props});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{users.map((user) => (
|
||||
<UserItem
|
||||
key={user.id}
|
||||
user={user}
|
||||
containerStyle={style.container}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
<TouchableOpacity onPress={openUserProfile}>
|
||||
<UserItem
|
||||
user={user}
|
||||
containerStyle={containerStyle}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const UsersList = ({channelId, location, users}: Props) => {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [direction, setDirection] = useState<'down' | 'up'>('down');
|
||||
const listRef = useRef<FlatList>(null);
|
||||
const prevOffset = useRef(0);
|
||||
const panResponder = useRef(PanResponder.create({
|
||||
onMoveShouldSetPanResponderCapture: (evt, g) => {
|
||||
const dir = prevOffset.current < g.dy ? 'down' : 'up';
|
||||
prevOffset.current = g.dy;
|
||||
if (!enabled && dir === 'up') {
|
||||
setEnabled(true);
|
||||
}
|
||||
setDirection(dir);
|
||||
return false;
|
||||
},
|
||||
})).current;
|
||||
|
||||
const onScroll = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
if (e.nativeEvent.contentOffset.y <= 0 && enabled && direction === 'down') {
|
||||
setEnabled(false);
|
||||
listRef.current?.scrollToOffset({animated: true, offset: 0});
|
||||
}
|
||||
}, [enabled, direction]);
|
||||
|
||||
const renderItem = useCallback(({item}) => (
|
||||
<Item
|
||||
channelId={channelId}
|
||||
location={location}
|
||||
user={item}
|
||||
containerStyle={style.container}
|
||||
/>
|
||||
), [channelId, location]);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={users}
|
||||
ref={listRef}
|
||||
renderItem={renderItem}
|
||||
onScroll={onScroll}
|
||||
overScrollMode={'always'}
|
||||
scrollEnabled={enabled}
|
||||
scrollEventThrottle={60}
|
||||
{...panResponder.panHandlers}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +1,38 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {
|
||||
Platform,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import {storeProfileLongPressTutorial} from '@actions/app/global';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
import {BotTag, GuestTag} from '@components/tag';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import TutorialHighlight from '@components/tutorial_highlight';
|
||||
import TutorialLongPress from '@components/tutorial_highlight/long_press';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
import {displayUsername, isGuest} from '@utils/user';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
isMyUser: boolean;
|
||||
highlight?: boolean;
|
||||
user: UserProfile;
|
||||
teammateNameDisplay: string;
|
||||
testID: string;
|
||||
onPress?: (user: UserProfile) => void;
|
||||
onLongPress: (user: UserProfile) => void;
|
||||
selectable: boolean;
|
||||
selected: boolean;
|
||||
tutorialWatched?: boolean;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -90,33 +98,73 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
|||
backgroundColor: theme.sidebarBg,
|
||||
borderWidth: 0,
|
||||
},
|
||||
tutorial: {
|
||||
top: Platform.select({ios: -74, default: -94}),
|
||||
},
|
||||
tutorialTablet: {
|
||||
top: -84,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default function UserListRow({
|
||||
id,
|
||||
isMyUser,
|
||||
highlight,
|
||||
user,
|
||||
teammateNameDisplay,
|
||||
testID,
|
||||
onPress,
|
||||
onLongPress,
|
||||
tutorialWatched = false,
|
||||
selectable,
|
||||
selected,
|
||||
enabled,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
const intl = useIntl();
|
||||
const isTablet = useIsTablet();
|
||||
const [showTutorial, setShowTutorial] = useState(false);
|
||||
const [itemBounds, setItemBounds] = useState<TutorialItemBounds>({startX: 0, startY: 0, endX: 0, endY: 0});
|
||||
const viewRef = useRef<View>(null);
|
||||
const style = getStyleFromTheme(theme);
|
||||
const {formatMessage} = intl;
|
||||
|
||||
const {username} = user;
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
if (onPress) {
|
||||
onPress(user);
|
||||
const startTutorial = () => {
|
||||
viewRef.current?.measureInWindow((x, y, w, h) => {
|
||||
const bounds: TutorialItemBounds = {
|
||||
startX: x - 20,
|
||||
startY: y,
|
||||
endX: x + w + 20,
|
||||
endY: y + h,
|
||||
};
|
||||
setShowTutorial(true);
|
||||
setItemBounds(bounds);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDismissTutorial = useCallback(() => {
|
||||
setShowTutorial(false);
|
||||
storeProfileLongPressTutorial();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let time: NodeJS.Timeout;
|
||||
if (highlight && !tutorialWatched) {
|
||||
time = setTimeout(startTutorial, 650);
|
||||
}
|
||||
return () => clearTimeout(time);
|
||||
}, [highlight, tutorialWatched]);
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
onPress?.(user);
|
||||
}, [onPress, user]);
|
||||
|
||||
const handleLongPress = useCallback(() => {
|
||||
onLongPress?.(user);
|
||||
}, [onLongPress, user]);
|
||||
|
||||
const iconStyle = useMemo(() => {
|
||||
return [style.selector, (selected && style.selectorFilled), (!enabled && style.selectorDisabled)];
|
||||
}, [style, selected, enabled]);
|
||||
|
|
@ -153,65 +201,80 @@ export default function UserListRow({
|
|||
const profilePictureTestID = `${itemTestID}.profile_picture`;
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={handlePress}
|
||||
underlayColor={changeOpacity(theme.centerChannelColor, 0.16)}
|
||||
>
|
||||
<View
|
||||
style={style.container}
|
||||
testID={itemTestID}
|
||||
<>
|
||||
<TouchableWithFeedback
|
||||
onLongPress={handleLongPress}
|
||||
onPress={handlePress}
|
||||
underlayColor={changeOpacity(theme.centerChannelColor, 0.16)}
|
||||
>
|
||||
<View style={style.profileContainer}>
|
||||
<ProfilePicture
|
||||
author={user}
|
||||
size={32}
|
||||
iconSize={24}
|
||||
testID={profilePictureTestID}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.textContainer}>
|
||||
<View style={style.indicatorContainer}>
|
||||
<Text
|
||||
style={style.username}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
testID={displayNameTestID}
|
||||
>
|
||||
{usernameDisplay}
|
||||
</Text>
|
||||
<BotTag
|
||||
show={Boolean(user.is_bot)}
|
||||
/>
|
||||
<GuestTag
|
||||
show={isGuest(user.roles)}
|
||||
<View
|
||||
ref={viewRef}
|
||||
style={style.container}
|
||||
testID={itemTestID}
|
||||
>
|
||||
<View style={style.profileContainer}>
|
||||
<ProfilePicture
|
||||
author={user}
|
||||
size={32}
|
||||
iconSize={24}
|
||||
testID={profilePictureTestID}
|
||||
/>
|
||||
</View>
|
||||
{showTeammateDisplay &&
|
||||
<View>
|
||||
<Text
|
||||
style={style.displayName}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{teammateDisplay}
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
{user.delete_at > 0 &&
|
||||
<View>
|
||||
<Text
|
||||
style={style.deactivated}
|
||||
>
|
||||
{formatMessage({id: 'mobile.user_list.deactivated', defaultMessage: 'Deactivated'})}
|
||||
</Text>
|
||||
<View style={style.textContainer}>
|
||||
<View style={style.indicatorContainer}>
|
||||
<Text
|
||||
style={style.username}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
testID={displayNameTestID}
|
||||
>
|
||||
{usernameDisplay}
|
||||
</Text>
|
||||
<BotTag
|
||||
show={Boolean(user.is_bot)}
|
||||
/>
|
||||
<GuestTag
|
||||
show={isGuest(user.roles)}
|
||||
/>
|
||||
</View>
|
||||
{showTeammateDisplay &&
|
||||
<View>
|
||||
<Text
|
||||
style={style.displayName}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{teammateDisplay}
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
{user.delete_at > 0 &&
|
||||
<View>
|
||||
<Text
|
||||
style={style.deactivated}
|
||||
>
|
||||
{formatMessage({id: 'mobile.user_list.deactivated', defaultMessage: 'Deactivated'})}
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
{selectable &&
|
||||
<Icon/>
|
||||
}
|
||||
</View>
|
||||
{selectable &&
|
||||
<Icon/>
|
||||
}
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
</TouchableWithFeedback>
|
||||
{showTutorial &&
|
||||
<TutorialHighlight
|
||||
itemBounds={itemBounds}
|
||||
onDismiss={handleDismissTutorial}
|
||||
>
|
||||
<TutorialLongPress
|
||||
message={intl.formatMessage({id: 'user.tutorial.long_press', defaultMessage: "Long-press on an item to view a user's profile"})}
|
||||
style={isTablet ? style.tutorialTablet : style.tutorial}
|
||||
/>
|
||||
</TutorialHighlight>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ export const SYSTEM_IDENTIFIERS = {
|
|||
export const GLOBAL_IDENTIFIERS = {
|
||||
DEVICE_TOKEN: 'deviceToken',
|
||||
MULTI_SERVER_TUTORIAL: 'multiServerTutorial',
|
||||
PROFILE_LONG_PRESS_TUTORIAL: 'profileLongPressTutorial',
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -26,4 +26,5 @@ export default keyMirror({
|
|||
POST_LIST_SCROLL_TO_BOTTOM: null,
|
||||
SWIPEABLE: null,
|
||||
ITEM_IN_VIEWPORT: null,
|
||||
SEND_TO_POST_DRAFT: null,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -131,5 +131,4 @@ export const NOT_READY = [
|
|||
CREATE_TEAM,
|
||||
INTEGRATION_SELECTOR,
|
||||
INTERACTIVE_DIALOG,
|
||||
USER_PROFILE,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {of as of$} from 'rxjs';
|
|||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {GLOBAL_IDENTIFIERS, MM_TABLES} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
|
||||
import type GlobalModel from '@typings/database/models/app/global';
|
||||
|
||||
|
|
@ -26,3 +27,14 @@ export const observeMultiServerTutorial = (appDatabase: Database) => {
|
|||
switchMap((v) => of$(Boolean(v))),
|
||||
);
|
||||
};
|
||||
|
||||
export const observeProfileLongPresTutorial = () => {
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (!appDatabase) {
|
||||
return of$(false);
|
||||
}
|
||||
return appDatabase.get<GlobalModel>(GLOBAL).query(Q.where('id', GLOBAL_IDENTIFIERS.PROFILE_LONG_PRESS_TUTORIAL), Q.take(1)).observe().pipe(
|
||||
switchMap((result) => (result.length ? result[0].observe() : of$(false))),
|
||||
switchMap((v) => of$(Boolean(v))),
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ import {queryPreferencesByCategoryAndName} from './preference';
|
|||
import {observeConfig, observeCurrentUserId, observeLicense, getCurrentUserId, getConfig, getLicense} from './system';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership';
|
||||
import type TeamMembershipModel from '@typings/database/models/servers/team_membership';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
const {SERVER: {USER}} = MM_TABLES;
|
||||
const {SERVER: {CHANNEL_MEMBERSHIP, USER, TEAM_MEMBERSHIP}} = MM_TABLES;
|
||||
export const getUserById = async (database: Database, userId: string) => {
|
||||
try {
|
||||
const userRecord = (await database.get<UserModel>(USER).find(userId));
|
||||
|
|
@ -96,3 +98,21 @@ export const queryUsersByIdsOrUsernames = (database: Database, ids: string[], us
|
|||
Q.where('username', Q.oneOf(usernames)),
|
||||
));
|
||||
};
|
||||
|
||||
export const observeUserIsTeamAdmin = (database: Database, userId: string, teamId: string) => {
|
||||
const id = `${teamId}-${userId}`;
|
||||
return database.get<TeamMembershipModel>(TEAM_MEMBERSHIP).query(
|
||||
Q.where('id', Q.eq(id)),
|
||||
).observe().pipe(
|
||||
switchMap((tm) => of$(tm.length ? tm[0].schemeAdmin : false)),
|
||||
);
|
||||
};
|
||||
|
||||
export const observeUserIsChannelAdmin = (database: Database, userId: string, teamId: string) => {
|
||||
const id = `${teamId}-${userId}`;
|
||||
return database.get<ChannelMembershipModel>(CHANNEL_MEMBERSHIP).query(
|
||||
Q.where('id', Q.eq(id)),
|
||||
).observe().pipe(
|
||||
switchMap((tm) => of$(tm.length ? tm[0].schemeAdmin : false)),
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -397,6 +397,8 @@ function AppsFormComponent({
|
|||
baseTextStyle={style.errorLabel}
|
||||
textStyles={getMarkdownTextStyles(theme)}
|
||||
blockStyles={getMarkdownBlockStyles(theme)}
|
||||
location=''
|
||||
disableAtMentions={true}
|
||||
value={error}
|
||||
theme={theme}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -157,6 +157,8 @@ function AppsFormField({
|
|||
<Markdown
|
||||
value={field.description}
|
||||
mentionKeys={[]}
|
||||
disableAtMentions={true}
|
||||
location=''
|
||||
blockStyles={getMarkdownBlockStyles(theme)}
|
||||
textStyles={getMarkdownTextStyles(theme)}
|
||||
baseTextStyle={style.markdownFieldText}
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ const DirectChannel = ({channel, currentUserId, isBot, members, theme}: Props) =
|
|||
if (channel.type === General.DM_CHANNEL) {
|
||||
return (
|
||||
<Member
|
||||
channelId={channel.id}
|
||||
containerStyle={{height: 96}}
|
||||
member={channelMembers[0]}
|
||||
size={96}
|
||||
|
|
|
|||
|
|
@ -3,16 +3,17 @@
|
|||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {StyleProp, StyleSheet, ViewStyle} from 'react-native';
|
||||
import {Keyboard, StyleProp, StyleSheet, ViewStyle} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {showModal} from '@screens/navigation';
|
||||
import {Screens} from '@constants';
|
||||
import {openAsBottomSheet} from '@screens/navigation';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
size?: number;
|
||||
showStatus?: boolean;
|
||||
|
|
@ -28,30 +29,17 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
const Member = ({containerStyle, size = 72, showStatus = true, theme, user}: Props) => {
|
||||
const Member = ({channelId, containerStyle, size = 72, showStatus = true, theme, user}: Props) => {
|
||||
const intl = useIntl();
|
||||
const onPress = useCallback(() => {
|
||||
// const screen = Screens.USER_PROFILE;
|
||||
const screen = 'UserProfile';
|
||||
const screen = Screens.USER_PROFILE;
|
||||
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
|
||||
const passProps = {
|
||||
userId: user.id,
|
||||
};
|
||||
const closeButtonId = 'close-user-profile';
|
||||
const props = {closeButtonId, userId: user.id, channelId, location: Screens.CHANNEL};
|
||||
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: 'close-user-profile',
|
||||
icon: closeButton,
|
||||
testID: 'close.settings.button',
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
showModal(screen, title, passProps, options);
|
||||
}, [theme]);
|
||||
Keyboard.dismiss();
|
||||
openAsBottomSheet({screen, title, theme, closeButtonId, props});
|
||||
}, [theme, intl.locale]);
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@ import Emoji from '@components/emoji';
|
|||
import FormattedDate from '@components/formatted_date';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import Markdown from '@components/markdown';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
createdAt: number;
|
||||
createdBy: string;
|
||||
customStatus?: UserCustomStatus;
|
||||
|
|
@ -63,7 +65,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
},
|
||||
}));
|
||||
|
||||
const Extra = ({createdAt, createdBy, customStatus, header}: Props) => {
|
||||
const Extra = ({channelId, createdAt, createdBy, customStatus, header}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const blockStyles = getMarkdownBlockStyles(theme);
|
||||
|
|
@ -123,6 +125,7 @@ const Extra = ({createdAt, createdBy, customStatus, header}: Props) => {
|
|||
style={styles.extraHeading}
|
||||
/>
|
||||
<Markdown
|
||||
channelId={channelId}
|
||||
baseTextStyle={styles.header}
|
||||
blockStyles={blockStyles}
|
||||
disableBlockQuote={true}
|
||||
|
|
@ -130,6 +133,7 @@ const Extra = ({createdAt, createdBy, customStatus, header}: Props) => {
|
|||
disableGallery={true}
|
||||
disableHeading={true}
|
||||
disableTables={true}
|
||||
location={Screens.CHANNEL_INFO}
|
||||
textStyles={textStyles}
|
||||
layoutHeight={48}
|
||||
layoutWidth={100}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ type Props = {
|
|||
currentUserId: string;
|
||||
restrictDirectMessage: boolean;
|
||||
teammateNameDisplay: string;
|
||||
tutorialWatched: boolean;
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
|
|
@ -90,6 +91,7 @@ export default function CreateDirectMessage({
|
|||
currentUserId,
|
||||
restrictDirectMessage,
|
||||
teammateNameDisplay,
|
||||
tutorialWatched,
|
||||
}: Props) {
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
|
|
@ -397,6 +399,7 @@ export default function CreateDirectMessage({
|
|||
fetchMore={getProfiles}
|
||||
term={term}
|
||||
testID='create_direct_message.user_list'
|
||||
tutorialWatched={tutorialWatched}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {of as of$} from 'rxjs';
|
|||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {General} from '@constants';
|
||||
import {observeProfileLongPresTutorial} from '@queries/app/global';
|
||||
import {observeConfig, observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeTeammateNameDisplay} from '@queries/servers/user';
|
||||
|
||||
|
|
@ -23,6 +24,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
|||
teammateNameDisplay: observeTeammateNameDisplay(database),
|
||||
currentUserId: observeCurrentUserId(database),
|
||||
currentTeamId: observeCurrentTeamId(database),
|
||||
tutorialWatched: observeProfileLongPresTutorial(),
|
||||
restrictDirectMessage,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,14 +2,18 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {FlatList, Keyboard, ListRenderItemInfo, Platform, SectionList, SectionListData, Text, View} from 'react-native';
|
||||
|
||||
import {storeProfile} from '@actions/local/user';
|
||||
import Loading from '@components/loading';
|
||||
import NoResultsWithTerm from '@components/no_results_with_term';
|
||||
import UserListRow from '@components/user_list_row';
|
||||
import {General} from '@constants';
|
||||
import {General, Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useKeyboardHeight} from '@hooks/device';
|
||||
import {openAsBottomSheet} from '@screens/navigation';
|
||||
import {
|
||||
changeOpacity,
|
||||
makeStyleSheetFromTheme,
|
||||
|
|
@ -112,6 +116,7 @@ type Props = {
|
|||
selectedIds: {[id: string]: UserProfile};
|
||||
testID?: string;
|
||||
term?: string;
|
||||
tutorialWatched: boolean;
|
||||
}
|
||||
|
||||
export default function UserList({
|
||||
|
|
@ -125,8 +130,11 @@ export default function UserList({
|
|||
showNoResults,
|
||||
term,
|
||||
testID,
|
||||
tutorialWatched,
|
||||
}: Props) {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
const style = getStyleFromTheme(theme);
|
||||
const keyboardHeight = useKeyboardHeight();
|
||||
const noResutsStyle = useMemo(() => [
|
||||
|
|
@ -134,7 +142,24 @@ export default function UserList({
|
|||
{paddingBottom: keyboardHeight},
|
||||
], [style, keyboardHeight]);
|
||||
|
||||
const renderItem = useCallback(({item}: ListRenderItemInfo<UserProfile>) => {
|
||||
const openUserProfile = useCallback(async (profile: UserProfile) => {
|
||||
const {user} = await storeProfile(serverUrl, profile);
|
||||
if (user) {
|
||||
const screen = Screens.USER_PROFILE;
|
||||
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
|
||||
const closeButtonId = 'close-user-profile';
|
||||
const props = {
|
||||
closeButtonId,
|
||||
userId: user.id,
|
||||
location: Screens.USER_PROFILE,
|
||||
};
|
||||
|
||||
Keyboard.dismiss();
|
||||
openAsBottomSheet({screen, title, theme, closeButtonId, props});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderItem = useCallback(({item, index}: ListRenderItemInfo<UserProfile>) => {
|
||||
// The list will re-render when the selection changes because it's passed into the list as extraData
|
||||
const selected = Boolean(selectedIds[item.id]);
|
||||
const canAdd = Object.keys(selectedIds).length < General.MAX_USERS_IN_GM;
|
||||
|
|
@ -142,18 +167,21 @@ export default function UserList({
|
|||
return (
|
||||
<UserListRow
|
||||
key={item.id}
|
||||
highlight={index === 0}
|
||||
id={item.id}
|
||||
isMyUser={currentUserId === item.id}
|
||||
onPress={handleSelectProfile}
|
||||
onLongPress={openUserProfile}
|
||||
selectable={canAdd}
|
||||
selected={selected}
|
||||
enabled={canAdd}
|
||||
testID='create_direct_message.user_list.user_item'
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
tutorialWatched={tutorialWatched}
|
||||
user={item}
|
||||
/>
|
||||
);
|
||||
}, [selectedIds, currentUserId, handleSelectProfile, teammateNameDisplay]);
|
||||
}, [selectedIds, currentUserId, handleSelectProfile, teammateNameDisplay, tutorialWatched]);
|
||||
|
||||
const renderLoading = useCallback(() => {
|
||||
if (!loading) {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import type UserModel from '@typings/database/models/servers/user';
|
|||
type Props = {
|
||||
author?: UserModel;
|
||||
channel?: ChannelModel;
|
||||
location: string;
|
||||
post?: PostModel;
|
||||
teammateNameDisplay: string;
|
||||
testID: string;
|
||||
|
|
@ -119,7 +120,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
const Thread = ({author, channel, post, teammateNameDisplay, testID, thread}: Props) => {
|
||||
const Thread = ({author, channel, location, post, teammateNameDisplay, testID, thread}: Props) => {
|
||||
const intl = useIntl();
|
||||
const isTablet = useIsTablet();
|
||||
const theme = useTheme();
|
||||
|
|
@ -236,11 +237,15 @@ const Thread = ({author, channel, post, teammateNameDisplay, testID, thread}: Pr
|
|||
/>
|
||||
</View>
|
||||
{postBody}
|
||||
{Boolean(post && thread) &&
|
||||
<ThreadFooter
|
||||
author={author}
|
||||
channelId={post!.channelId}
|
||||
location={location}
|
||||
testID={`${threadItemTestId}.footer`}
|
||||
thread={thread}
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import type UserModel from '@typings/database/models/servers/user';
|
|||
|
||||
type Props = {
|
||||
author?: UserModel;
|
||||
channelId: string;
|
||||
location: string;
|
||||
participants: UserModel[];
|
||||
testID: string;
|
||||
thread: ThreadModel;
|
||||
|
|
@ -46,7 +48,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
const ThreadFooter = ({author, participants, testID, thread}: Props) => {
|
||||
const ThreadFooter = ({author, channelId, location, participants, testID, thread}: Props) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
|
|
@ -91,6 +93,8 @@ const ThreadFooter = ({author, participants, testID, thread}: Props) => {
|
|||
if (author && participantsList.length) {
|
||||
userAvatarsStack = (
|
||||
<UserAvatarsStack
|
||||
channelId={channelId}
|
||||
location={location}
|
||||
style={style.avatarsContainer}
|
||||
users={participantsList}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {FlatList, StyleSheet} from 'react-native';
|
|||
|
||||
import {fetchRefreshThreads, fetchThreads} from '@actions/remote/thread';
|
||||
import Loading from '@components/loading';
|
||||
import {General} from '@constants';
|
||||
import {General, Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
|
||||
|
|
@ -141,6 +141,7 @@ const ThreadsList = ({
|
|||
|
||||
const renderItem = useCallback(({item}) => (
|
||||
<Thread
|
||||
location={Screens.GLOBAL_THREADS}
|
||||
testID={testID}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
thread={item}
|
||||
|
|
|
|||
|
|
@ -164,30 +164,6 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
case Screens.SAVED_POSTS:
|
||||
screen = withServerDatabase((require('@screens/saved_posts').default));
|
||||
break;
|
||||
case Screens.SSO:
|
||||
screen = withIntl(require('@screens/sso').default);
|
||||
break;
|
||||
case Screens.THREAD:
|
||||
screen = withServerDatabase(require('@screens/thread').default);
|
||||
break;
|
||||
case Screens.THREAD_FOLLOW_BUTTON:
|
||||
Navigation.registerComponent(Screens.THREAD_FOLLOW_BUTTON, () => withServerDatabase(
|
||||
require('@screens/thread/thread_follow_button').default,
|
||||
));
|
||||
break;
|
||||
case Screens.SNACK_BAR: {
|
||||
const snackBarScreen = withServerDatabase(require('@screens/snack_bar').default);
|
||||
Navigation.registerComponent(Screens.SNACK_BAR, () =>
|
||||
Platform.select({
|
||||
default: snackBarScreen,
|
||||
ios: withSafeAreaInsets(snackBarScreen) as ComponentType,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case Screens.THREAD_OPTIONS:
|
||||
screen = withServerDatabase(require('@screens/thread_options').default);
|
||||
break;
|
||||
case Screens.SETTINGS:
|
||||
screen = withServerDatabase(require('@screens/settings').default);
|
||||
break;
|
||||
|
|
@ -206,6 +182,33 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
case Screens.SETTINGS_NOTIFICATION_AUTO_RESPONDER:
|
||||
screen = withServerDatabase(require('@screens/settings/notification_auto_responder').default);
|
||||
break;
|
||||
case Screens.SNACK_BAR: {
|
||||
const snackBarScreen = withServerDatabase(require('@screens/snack_bar').default);
|
||||
Navigation.registerComponent(Screens.SNACK_BAR, () =>
|
||||
Platform.select({
|
||||
default: snackBarScreen,
|
||||
ios: withSafeAreaInsets(snackBarScreen) as ComponentType,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case Screens.SSO:
|
||||
screen = withIntl(require('@screens/sso').default);
|
||||
break;
|
||||
case Screens.THREAD:
|
||||
screen = withServerDatabase(require('@screens/thread').default);
|
||||
break;
|
||||
case Screens.THREAD_FOLLOW_BUTTON:
|
||||
Navigation.registerComponent(Screens.THREAD_FOLLOW_BUTTON, () => withServerDatabase(
|
||||
require('@screens/thread/thread_follow_button').default,
|
||||
));
|
||||
break;
|
||||
case Screens.THREAD_OPTIONS:
|
||||
screen = withServerDatabase(require('@screens/thread_options').default);
|
||||
break;
|
||||
case Screens.USER_PROFILE:
|
||||
screen = withServerDatabase(require('@screens/user_profile').default);
|
||||
break;
|
||||
}
|
||||
|
||||
if (screen) {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ function DialogIntroductionText({value}: Props) {
|
|||
disableHashtags={true}
|
||||
disableAtMentions={true}
|
||||
disableChannelLink={true}
|
||||
location=''
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -111,6 +111,10 @@ export const bottomSheetModalOptions = (theme: Theme, closeButtonId?: string) =>
|
|||
enabled: false,
|
||||
},
|
||||
},
|
||||
modalPresentationStyle: Platform.select({
|
||||
ios: OptionsModalPresentationStyle.overFullScreen,
|
||||
default: OptionsModalPresentationStyle.overCurrentContext,
|
||||
}),
|
||||
modal: {swipeToDismiss: true},
|
||||
statusBar: {
|
||||
backgroundColor: null,
|
||||
|
|
@ -524,7 +528,7 @@ export function showModalOverCurrentContext(name: string, passProps = {}, option
|
|||
break;
|
||||
}
|
||||
const defaultOptions = {
|
||||
modalPresentationStyle: 'overCurrentContext',
|
||||
modalPresentationStyle: OptionsModalPresentationStyle.overCurrentContext,
|
||||
layout: {
|
||||
backgroundColor: 'transparent',
|
||||
componentBackgroundColor: 'transparent',
|
||||
|
|
@ -684,6 +688,28 @@ export async function dismissBottomSheet(alternativeScreen = Screens.BOTTOM_SHEE
|
|||
await EphemeralStore.waitUntilScreensIsRemoved(alternativeScreen);
|
||||
}
|
||||
|
||||
type AsBottomSheetArgs = {
|
||||
closeButtonId: string;
|
||||
props?: Record<string, any>;
|
||||
screen: typeof Screens[keyof typeof Screens];
|
||||
theme: Theme;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export async function openAsBottomSheet({closeButtonId, screen, theme, title, props}: AsBottomSheetArgs) {
|
||||
const {isSplitView} = await isRunningInSplitView();
|
||||
const isTablet = Device.IS_TABLET && !isSplitView;
|
||||
|
||||
if (isTablet) {
|
||||
showModal(screen, title, {
|
||||
closeButtonId,
|
||||
...props,
|
||||
}, bottomSheetModalOptions(theme, closeButtonId));
|
||||
} else {
|
||||
showModalOverCurrentContext(screen, props, bottomSheetModalOptions(theme));
|
||||
}
|
||||
}
|
||||
|
||||
export const showAppForm = async (form: AppForm, call: AppCallRequest) => {
|
||||
const passProps = {form, call};
|
||||
showModal(Screens.APPS_FORM, form.title || '', passProps);
|
||||
|
|
|
|||
|
|
@ -15,10 +15,11 @@ import type ReactionModel from '@typings/database/models/servers/reaction';
|
|||
|
||||
type Props = {
|
||||
initialEmoji: string;
|
||||
location: string;
|
||||
reactions?: ReactionModel[];
|
||||
}
|
||||
|
||||
const Reactions = ({initialEmoji, reactions}: Props) => {
|
||||
const Reactions = ({initialEmoji, location, reactions}: Props) => {
|
||||
const [sortedReactions, setSortedReactions] = useState(Array.from(new Set(reactions?.map((r) => getEmojiFirstAlias(r.emojiName)))));
|
||||
const [index, setIndex] = useState(sortedReactions.indexOf(initialEmoji));
|
||||
const reactionsByName = useMemo(() => {
|
||||
|
|
@ -56,11 +57,12 @@ const Reactions = ({initialEmoji, reactions}: Props) => {
|
|||
<EmojiAliases emoji={emojiAlias}/>
|
||||
<ReactorsList
|
||||
key={emojiAlias}
|
||||
location={location}
|
||||
reactions={reactionsByName.get(emojiAlias)!}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}, [index, reactions, sortedReactions]);
|
||||
}, [index, location, reactions, sortedReactions]);
|
||||
|
||||
useEffect(() => {
|
||||
// This helps keep the reactions in the same position at all times until unmounted
|
||||
|
|
|
|||
|
|
@ -13,10 +13,11 @@ import Reactor from './reactor';
|
|||
import type ReactionModel from '@typings/database/models/servers/reaction';
|
||||
|
||||
type Props = {
|
||||
location: string;
|
||||
reactions: ReactionModel[];
|
||||
}
|
||||
|
||||
const ReactorsList = ({reactions}: Props) => {
|
||||
const ReactorsList = ({location, reactions}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [direction, setDirection] = useState<'down' | 'up'>('down');
|
||||
|
|
@ -35,7 +36,10 @@ const ReactorsList = ({reactions}: Props) => {
|
|||
})).current;
|
||||
|
||||
const renderItem = useCallback(({item}) => (
|
||||
<Reactor reaction={item}/>
|
||||
<Reactor
|
||||
location={location}
|
||||
reaction={item}
|
||||
/>
|
||||
), [reactions]);
|
||||
|
||||
const onScroll = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@
|
|||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observePost} from '@queries/servers/post';
|
||||
import {observeUser} from '@queries/servers/user';
|
||||
import {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
|
|
@ -12,6 +15,9 @@ import Reactor from './reactor';
|
|||
import type ReactionModel from '@typings/database/models/servers/reaction';
|
||||
|
||||
const enhance = withObservables(['reaction'], ({database, reaction}: {reaction: ReactionModel} & WithDatabaseArgs) => ({
|
||||
channelId: observePost(database, reaction.postId).pipe(
|
||||
switchMap((p) => of$(p?.channelId)),
|
||||
),
|
||||
user: observeUser(database, reaction.userId),
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,19 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet} from 'react-native';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Keyboard, StyleSheet, TouchableOpacity} from 'react-native';
|
||||
|
||||
import UserItem from '@components/user_item';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {dismissBottomSheet, openAsBottomSheet} from '@screens/navigation';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
location: string;
|
||||
user?: UserModel;
|
||||
}
|
||||
|
||||
|
|
@ -18,12 +24,29 @@ const style = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
const Reactor = ({user}: Props) => {
|
||||
const Reactor = ({channelId, location, user}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const openUserProfile = async () => {
|
||||
if (user) {
|
||||
await dismissBottomSheet(Screens.REACTIONS);
|
||||
const screen = Screens.USER_PROFILE;
|
||||
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
|
||||
const closeButtonId = 'close-user-profile';
|
||||
const props = {closeButtonId, location, userId: user.id, channelId};
|
||||
|
||||
Keyboard.dismiss();
|
||||
openAsBottomSheet({screen, title, theme, closeButtonId, props});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<UserItem
|
||||
containerStyle={style.container}
|
||||
user={user}
|
||||
/>
|
||||
<TouchableOpacity onPress={openUserProfile}>
|
||||
<UserItem
|
||||
containerStyle={style.container}
|
||||
user={user}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
60
app/screens/user_profile/index.ts
Normal file
60
app/screens/user_profile/index.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {map, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {General, Preferences} from '@constants';
|
||||
import {getPreferenceAsBool} from '@helpers/api/preference';
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {observeConfigBooleanValue, observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeTeammateNameDisplay, observeUser, observeUserIsChannelAdmin, observeUserIsTeamAdmin} from '@queries/servers/user';
|
||||
import {isSystemAdmin} from '@utils/user';
|
||||
|
||||
import UserProfile from './user_profile';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type EnhancedProps = WithDatabaseArgs & {
|
||||
userId: string;
|
||||
channelId?: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables([], ({channelId, database, userId}: EnhancedProps) => {
|
||||
const currentUserId = observeCurrentUserId(database);
|
||||
const channel = channelId ? observeChannel(database, channelId) : of$(undefined);
|
||||
const user = observeUser(database, userId);
|
||||
const teammateDisplayName = observeTeammateNameDisplay(database);
|
||||
const isChannelAdmin = channelId ? observeUserIsChannelAdmin(database, userId, channelId) : of$(false);
|
||||
const isDirectMessage = channelId ? channel.pipe(
|
||||
switchMap((c) => of$(c?.type === General.DM_CHANNEL)),
|
||||
) : of$(false);
|
||||
const teamId = channel.pipe(switchMap((c) => (c?.teamId ? of$(c.teamId) : observeCurrentTeamId(database))));
|
||||
const isTeamAdmin = teamId.pipe(switchMap((id) => observeUserIsTeamAdmin(database, userId, id)));
|
||||
const systemAdmin = user.pipe(switchMap((u) => of$(u?.roles ? isSystemAdmin(u.roles) : false)));
|
||||
const enablePostIconOverride = observeConfigBooleanValue(database, 'EnablePostIconOverride');
|
||||
const enablePostUsernameOverride = observeConfigBooleanValue(database, 'EnablePostUsernameOverride');
|
||||
const preferences = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).
|
||||
observeWithColumns(['value']);
|
||||
const isMilitaryTime = preferences.pipe(map((prefs) => getPreferenceAsBool(prefs, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', false)));
|
||||
|
||||
return {
|
||||
currentUserId,
|
||||
enablePostIconOverride,
|
||||
enablePostUsernameOverride,
|
||||
isChannelAdmin,
|
||||
isDirectMessage,
|
||||
isMilitaryTime,
|
||||
isSystemAdmin: systemAdmin,
|
||||
isTeamAdmin,
|
||||
teamId,
|
||||
teammateDisplayName,
|
||||
user,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(UserProfile));
|
||||
|
||||
43
app/screens/user_profile/label.tsx
Normal file
43
app/screens/user_profile/label.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
marginVertical: 8,
|
||||
},
|
||||
description: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 200),
|
||||
},
|
||||
title: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
marginBottom: 2,
|
||||
...typography('Body', 50, 'SemiBold'),
|
||||
},
|
||||
}));
|
||||
|
||||
const UserProfileLabel = ({title, description}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
<Text style={styles.description}>{description}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProfileLabel;
|
||||
113
app/screens/user_profile/options.tsx
Normal file
113
app/screens/user_profile/options.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {DeviceEventEmitter, StyleSheet, TouchableOpacity, View} from 'react-native';
|
||||
|
||||
import {createDirectChannel, switchToChannelById} from '@actions/remote/channel';
|
||||
import {buttonBackgroundStyle, buttonTextStyle} from '@app/utils/buttonStyles';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import OptionBox, {OPTIONS_HEIGHT} from '@components/option_box';
|
||||
import {Events, Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
|
||||
export type OptionsType = 'all' | 'message';
|
||||
|
||||
type Props = {
|
||||
location: string;
|
||||
type: OptionsType;
|
||||
userId: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
height: OPTIONS_HEIGHT,
|
||||
marginBottom: 20,
|
||||
width: '100%',
|
||||
},
|
||||
divider: {
|
||||
marginRight: 8,
|
||||
},
|
||||
icon: {
|
||||
fontSize: 24,
|
||||
lineHeight: 22,
|
||||
},
|
||||
singleButton: {
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
},
|
||||
singleContainer: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 20,
|
||||
width: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
const UserProfileOptions = ({location, type, userId, username}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const buttonStyle = useMemo(() => {
|
||||
return buttonBackgroundStyle(theme, 'lg', 'tertiary', 'default');
|
||||
}, [theme]);
|
||||
|
||||
const mentionUser = useCallback(async () => {
|
||||
await dismissBottomSheet(Screens.USER_PROFILE);
|
||||
DeviceEventEmitter.emit(Events.SEND_TO_POST_DRAFT, {location, text: `@${username}`});
|
||||
}, [location, username]);
|
||||
|
||||
const openChannel = useCallback(async () => {
|
||||
await dismissBottomSheet(Screens.USER_PROFILE);
|
||||
const {data} = await createDirectChannel(serverUrl, userId);
|
||||
if (data) {
|
||||
switchToChannelById(serverUrl, data.id);
|
||||
}
|
||||
}, [userId, serverUrl]);
|
||||
|
||||
if (type === 'all') {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<OptionBox
|
||||
iconName='send'
|
||||
onPress={openChannel}
|
||||
text={intl.formatMessage({id: 'channel_info.send_mesasge', defaultMessage: 'Send message'})}
|
||||
/>
|
||||
<View style={styles.divider}/>
|
||||
<OptionBox
|
||||
iconName='at'
|
||||
onPress={mentionUser}
|
||||
text={intl.formatMessage({id: 'channel_info.mention', defaultMessage: 'Mention'})}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.singleContainer}>
|
||||
<TouchableOpacity
|
||||
style={[buttonStyle, styles.singleButton]}
|
||||
onPress={openChannel}
|
||||
>
|
||||
<CompassIcon
|
||||
color={theme.buttonBg}
|
||||
name='send'
|
||||
style={styles.icon}
|
||||
/>
|
||||
<FormattedText
|
||||
id='channel_info.send_a_mesasge'
|
||||
defaultMessage='Send a message'
|
||||
style={[buttonTextStyle(theme, 'lg', 'tertiary', 'default'), {marginLeft: 8}]}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProfileOptions;
|
||||
50
app/screens/user_profile/title/avatar.tsx
Normal file
50
app/screens/user_profile/title/avatar.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import ProfilePicture from '@app/components/profile_picture';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
enablePostIconOverride: boolean;
|
||||
user: UserModel;
|
||||
userIconOverride?: string;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
avatar: {
|
||||
borderRadius: 48,
|
||||
height: 96,
|
||||
width: 96,
|
||||
},
|
||||
});
|
||||
|
||||
const UserProfileAvatar = ({enablePostIconOverride, user, userIconOverride}: Props) => {
|
||||
if (enablePostIconOverride && userIconOverride) {
|
||||
return (
|
||||
<View style={styles.avatar}>
|
||||
<FastImage
|
||||
style={styles.avatar}
|
||||
source={{uri: userIconOverride}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ProfilePicture
|
||||
author={user}
|
||||
showStatus={true}
|
||||
size={96}
|
||||
statusSize={24}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProfileAvatar;
|
||||
108
app/screens/user_profile/title/index.tsx
Normal file
108
app/screens/user_profile/title/index.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
import UserProfileAvatar from './avatar';
|
||||
import UserProfileTag from './tag';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
enablePostIconOverride: boolean;
|
||||
enablePostUsernameOverride: boolean;
|
||||
isChannelAdmin: boolean;
|
||||
isSystemAdmin: boolean;
|
||||
isTeamAdmin: boolean;
|
||||
teammateDisplayName: string;
|
||||
user: UserModel;
|
||||
userIconOverride?: string;
|
||||
usernameOverride?: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 20,
|
||||
},
|
||||
details: {
|
||||
marginLeft: 24,
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
},
|
||||
displayName: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 600, 'SemiBold'),
|
||||
},
|
||||
username: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
...typography('Body', 200),
|
||||
},
|
||||
tablet: {
|
||||
marginTop: 20,
|
||||
},
|
||||
}));
|
||||
|
||||
const UserProfileTitle = ({
|
||||
enablePostIconOverride, enablePostUsernameOverride,
|
||||
isChannelAdmin, isSystemAdmin, isTeamAdmin,
|
||||
teammateDisplayName, user, userIconOverride, usernameOverride,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const isTablet = useIsTablet();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const override = enablePostUsernameOverride && usernameOverride;
|
||||
|
||||
let displayName;
|
||||
if (override) {
|
||||
displayName = usernameOverride;
|
||||
} else {
|
||||
displayName = displayUsername(user, intl.locale, teammateDisplayName, false);
|
||||
}
|
||||
|
||||
const hideUsername = override || (displayName && displayName === user.username);
|
||||
const prefix = hideUsername ? '@' : '';
|
||||
|
||||
return (
|
||||
<View style={[styles.container, isTablet && styles.tablet]}>
|
||||
<UserProfileAvatar
|
||||
enablePostIconOverride={enablePostIconOverride}
|
||||
user={user}
|
||||
userIconOverride={userIconOverride}
|
||||
/>
|
||||
<View style={styles.details}>
|
||||
<UserProfileTag
|
||||
isBot={user.isBot || Boolean(userIconOverride || usernameOverride)}
|
||||
isChannelAdmin={isChannelAdmin}
|
||||
isGuest={user.isGuest}
|
||||
isSystemAdmin={isSystemAdmin}
|
||||
isTeamAdmin={isTeamAdmin}
|
||||
/>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={styles.displayName}
|
||||
>
|
||||
{`${prefix}${displayName}`}
|
||||
</Text>
|
||||
{!hideUsername &&
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={styles.username}
|
||||
>
|
||||
{`@${user.username}`}
|
||||
</Text>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProfileTitle;
|
||||
67
app/screens/user_profile/title/tag.tsx
Normal file
67
app/screens/user_profile/title/tag.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet} from 'react-native';
|
||||
|
||||
import Tag, {BotTag, GuestTag} from '@components/tag';
|
||||
|
||||
type Props = {
|
||||
isBot: boolean;
|
||||
isChannelAdmin: boolean;
|
||||
isGuest: boolean;
|
||||
isSystemAdmin: boolean;
|
||||
isTeamAdmin: boolean;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
tag: {
|
||||
alignSelf: 'flex-start',
|
||||
paddingHorizontal: 6,
|
||||
marginBottom: 4,
|
||||
},
|
||||
});
|
||||
|
||||
const UserProfileTag = ({isBot, isChannelAdmin, isGuest, isSystemAdmin, isTeamAdmin}: Props) => {
|
||||
if (isBot) {
|
||||
return (<BotTag style={styles.tag}/>);
|
||||
}
|
||||
|
||||
if (isGuest) {
|
||||
return (<GuestTag style={styles.tag}/>);
|
||||
}
|
||||
|
||||
if (isSystemAdmin) {
|
||||
return (
|
||||
<Tag
|
||||
id='user_profile.system_admin'
|
||||
defaultMessage='System Admin'
|
||||
style={styles.tag}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isTeamAdmin) {
|
||||
return (
|
||||
<Tag
|
||||
id='user_profile.team_admin'
|
||||
defaultMessage='Team Admin'
|
||||
style={styles.tag}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isChannelAdmin) {
|
||||
return (
|
||||
<Tag
|
||||
id='user_profile.channel_admin'
|
||||
defaultMessage='Channel Admin'
|
||||
style={styles.tag}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default UserProfileTag;
|
||||
160
app/screens/user_profile/user_profile.tsx
Normal file
160
app/screens/user_profile/user_profile.tsx
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import React, {useEffect, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {fetchTeamAndChannelMembership} from '@actions/remote/user';
|
||||
import {useServerUrl} from '@app/context/server';
|
||||
import {Screens} from '@constants';
|
||||
import {getLocaleFromLanguage} from '@i18n';
|
||||
import BottomSheet from '@screens/bottom_sheet';
|
||||
import {getUserTimezone} from '@utils/user';
|
||||
|
||||
import UserProfileLabel from './label';
|
||||
import UserProfileOptions, {OptionsType} from './options';
|
||||
import UserProfileTitle from './title';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
channelId?: string;
|
||||
currentUserId: string;
|
||||
enablePostIconOverride: boolean;
|
||||
enablePostUsernameOverride: boolean;
|
||||
isChannelAdmin: boolean;
|
||||
isDirectMessage: boolean;
|
||||
isMilitaryTime: boolean;
|
||||
isSystemAdmin: boolean;
|
||||
isTeamAdmin: boolean;
|
||||
location: string;
|
||||
teamId: string;
|
||||
teammateDisplayName: string;
|
||||
user: UserModel;
|
||||
userIconOverride?: string;
|
||||
usernameOverride?: string;
|
||||
}
|
||||
|
||||
const TITLE_HEIGHT = 118;
|
||||
const OPTIONS_HEIGHT = 82;
|
||||
const SINGLE_OPTION_HEIGHT = 68;
|
||||
const LABEL_HEIGHT = 58;
|
||||
const EXTRA_HEIGHT = 60;
|
||||
|
||||
const UserProfile = ({
|
||||
channelId, currentUserId, enablePostIconOverride, enablePostUsernameOverride,
|
||||
isChannelAdmin, isDirectMessage, isMilitaryTime, isSystemAdmin, isTeamAdmin,
|
||||
location, teamId, teammateDisplayName,
|
||||
user, userIconOverride, usernameOverride,
|
||||
}: Props) => {
|
||||
const {formatMessage, locale} = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const insets = useSafeAreaInsets();
|
||||
const channelContext = [Screens.CHANNEL, Screens.THREAD].includes(location);
|
||||
const showOptions: OptionsType = channelContext && !user.isBot ? 'all' : 'message';
|
||||
const override = Boolean(userIconOverride || usernameOverride);
|
||||
const timezone = getUserTimezone(user);
|
||||
let localTime: string|undefined;
|
||||
if (timezone) {
|
||||
moment.locale(getLocaleFromLanguage(locale).toLowerCase());
|
||||
let format = 'H:mm';
|
||||
if (!isMilitaryTime) {
|
||||
const localeFormat = moment.localeData().longDateFormat('LT');
|
||||
format = localeFormat?.includes('A') ? localeFormat : 'h:mm A';
|
||||
}
|
||||
localTime = moment.tz(Date.now(), timezone).format(format);
|
||||
}
|
||||
|
||||
const snapPoints = useMemo(() => {
|
||||
let initial = TITLE_HEIGHT;
|
||||
if ((!isDirectMessage || !channelContext) && !override) {
|
||||
initial += showOptions === 'all' ? OPTIONS_HEIGHT : SINGLE_OPTION_HEIGHT;
|
||||
}
|
||||
|
||||
let labels = 0;
|
||||
if (!override && !user.isBot) {
|
||||
if (user.nickname) {
|
||||
labels += 1;
|
||||
}
|
||||
|
||||
if (user.position) {
|
||||
labels += 1;
|
||||
}
|
||||
|
||||
if (localTime) {
|
||||
labels += 1;
|
||||
}
|
||||
initial += (labels * LABEL_HEIGHT);
|
||||
}
|
||||
|
||||
return [initial + insets.bottom + EXTRA_HEIGHT, 10];
|
||||
}, [
|
||||
isChannelAdmin, isDirectMessage, isSystemAdmin,
|
||||
isTeamAdmin, user, localTime, insets.bottom, override,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUserId !== user.id) {
|
||||
fetchTeamAndChannelMembership(serverUrl, user.id, teamId, channelId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<>
|
||||
<UserProfileTitle
|
||||
enablePostIconOverride={enablePostIconOverride}
|
||||
enablePostUsernameOverride={enablePostUsernameOverride}
|
||||
isChannelAdmin={isChannelAdmin}
|
||||
isSystemAdmin={isSystemAdmin}
|
||||
isTeamAdmin={isTeamAdmin}
|
||||
teammateDisplayName={teammateDisplayName}
|
||||
user={user}
|
||||
userIconOverride={userIconOverride}
|
||||
usernameOverride={usernameOverride}
|
||||
/>
|
||||
{(!isDirectMessage || !channelContext) && !override &&
|
||||
<UserProfileOptions
|
||||
location={location}
|
||||
type={showOptions}
|
||||
username={user.username}
|
||||
userId={user.id}
|
||||
/>
|
||||
}
|
||||
{Boolean(user.nickname) && !override && !user.isBot &&
|
||||
<UserProfileLabel
|
||||
description={user.nickname}
|
||||
title={formatMessage({id: 'channel_info.nickname', defaultMessage: 'Nickname'})}
|
||||
/>
|
||||
}
|
||||
{Boolean(user.position) && !override && !user.isBot &&
|
||||
<UserProfileLabel
|
||||
description={user.position}
|
||||
title={formatMessage({id: 'channel_info.position', defaultMessage: 'Position'})}
|
||||
/>
|
||||
}
|
||||
{Boolean(localTime) && !override && !user.isBot &&
|
||||
<UserProfileLabel
|
||||
description={localTime!}
|
||||
title={formatMessage({id: 'channel_info.local_time', defaultMessage: 'Local Time'})}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<BottomSheet
|
||||
renderContent={renderContent}
|
||||
closeButtonId='close-post-options'
|
||||
componentId={Screens.USER_PROFILE}
|
||||
initialSnapIndex={0}
|
||||
snapPoints={snapPoints}
|
||||
testID='post_options'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProfile;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Keyboard} from 'react-native';
|
||||
import {Keyboard, Platform} from 'react-native';
|
||||
import {OptionsModalPresentationStyle} from 'react-native-navigation';
|
||||
|
||||
import {dismissAllModals, showModalOverCurrentContext} from '@screens/navigation';
|
||||
|
|
@ -23,7 +23,10 @@ export const displayPermalink = async (teamName: string, postId: string, openAsP
|
|||
};
|
||||
|
||||
const options = {
|
||||
modalPresentationStyle: OptionsModalPresentationStyle.overFullScreen,
|
||||
modalPresentationStyle: Platform.select({
|
||||
ios: OptionsModalPresentationStyle.overFullScreen,
|
||||
default: OptionsModalPresentationStyle.overCurrentContext,
|
||||
}),
|
||||
layout: {
|
||||
componentBackgroundColor: changeOpacity('#000', 0.2),
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue