forked from Ivasoft/mattermost-mobile
[Gekidou] [MM-39718] Add Create DM screen (#5900)
* Add Create DM screen * Add channel toggle and minor improvements * Fix tests and apply new UI * Address feedback UX feedback and fix missing menu item by adding another item height * Add display name to channels and piggyback improvement on fetchUserByIds action and translations fix * Address feedback * Fix hardcoded colors Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
committed by
GitHub
parent
eec536a61b
commit
b27ebce2e0
@@ -7,11 +7,13 @@ import {IntlShape} from 'react-intl';
|
||||
|
||||
import {storeCategories} from '@actions/local/category';
|
||||
import {storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
|
||||
import {General} from '@constants';
|
||||
import {General, Preferences} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {privateChannelJoinPrompt} from '@helpers/api/channel';
|
||||
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {prepareMyChannelsForTeam, queryChannelById, queryChannelByName, queryMyChannel} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {queryCommonSystemValues, queryCurrentTeamId, queryCurrentUserId} from '@queries/servers/system';
|
||||
import {prepareMyTeams, queryNthLastChannelFromTeam, queryMyTeamById, queryTeamById, queryTeamByName} from '@queries/servers/team';
|
||||
import {queryCurrentUser} from '@queries/servers/user';
|
||||
@@ -26,6 +28,7 @@ import {addUserToTeam, fetchTeamByName, removeUserFromTeam} from './team';
|
||||
import {fetchProfilesPerChannels, fetchUsersByIds} from './user';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type ChannelInfoModel from '@typings/database/models/servers/channel_info';
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
import type MyTeamModel from '@typings/database/models/servers/my_team';
|
||||
@@ -547,6 +550,69 @@ export const switchToChannelByName = async (serverUrl: string, channelName: stri
|
||||
}
|
||||
};
|
||||
|
||||
export const createDirectChannel = async (serverUrl: string, userId: string, displayName = '') => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const currentUser = await queryCurrentUser(operator.database);
|
||||
if (!currentUser) {
|
||||
return {error: 'Cannot get the current user'};
|
||||
}
|
||||
const created = await client.createDirectChannel([userId, currentUser.id]);
|
||||
|
||||
if (displayName) {
|
||||
created.display_name = displayName;
|
||||
} else {
|
||||
const preferences = await queryPreferencesByCategoryAndName(operator.database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT);
|
||||
const system = await queryCommonSystemValues(operator.database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], system.config, system.license);
|
||||
const {directChannels} = await fetchMissingSidebarInfo(serverUrl, [created], currentUser.locale, teammateDisplayNameSetting, currentUser.id, true);
|
||||
created.display_name = directChannels?.[0].display_name || created.display_name;
|
||||
}
|
||||
|
||||
const member = {
|
||||
channel_id: created.id,
|
||||
user_id: currentUser.id,
|
||||
roles: `${General.CHANNEL_USER_ROLE}`,
|
||||
last_viewed_at: 0,
|
||||
msg_count: 0,
|
||||
mention_count: 0,
|
||||
msg_count_root: 0,
|
||||
mention_count_root: 0,
|
||||
notify_props: {desktop: 'default' as const, mark_unread: 'all' as const},
|
||||
last_update_at: created.create_at,
|
||||
};
|
||||
|
||||
const models = [];
|
||||
const channelPromises = await prepareMyChannelsForTeam(operator, '', [created], [member, {...member, user_id: userId}]);
|
||||
if (channelPromises) {
|
||||
const channelModels = await Promise.all(channelPromises);
|
||||
const flattenedChannelModels = channelModels.flat();
|
||||
if (flattenedChannelModels.length) {
|
||||
models.push(...flattenedChannelModels);
|
||||
}
|
||||
}
|
||||
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
fetchRolesIfNeeded(serverUrl, member.roles.split(' '));
|
||||
return {data: created};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
export const fetchChannels = async (serverUrl: string, teamId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE) => {
|
||||
let client: Client;
|
||||
try {
|
||||
@@ -565,6 +631,33 @@ export const fetchChannels = async (serverUrl: string, teamId: string, page = 0,
|
||||
}
|
||||
};
|
||||
|
||||
export const makeDirectChannel = async (serverUrl: string, userId: string, displayName = '', shouldSwitchToChannel = true) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const currentUserId = await queryCurrentUserId(operator.database);
|
||||
const channelName = getDirectChannelName(userId, currentUserId);
|
||||
let channel: Channel|ChannelModel|undefined = await queryChannelByName(operator.database, channelName);
|
||||
let result: {data?: Channel|ChannelModel; error?: any};
|
||||
if (channel) {
|
||||
result = {data: channel};
|
||||
} else {
|
||||
result = await createDirectChannel(serverUrl, userId, displayName);
|
||||
channel = result.data;
|
||||
}
|
||||
|
||||
if (channel && shouldSwitchToChannel) {
|
||||
switchToChannelById(serverUrl, channel.id);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
export const fetchArchivedChannels = async (serverUrl: string, teamId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE) => {
|
||||
let client: Client;
|
||||
try {
|
||||
@@ -583,6 +676,70 @@ export const fetchArchivedChannels = async (serverUrl: string, teamId: string, p
|
||||
}
|
||||
};
|
||||
|
||||
export const createGroupChannel = async (serverUrl: string, userIds: string[]) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
try {
|
||||
const currentUser = await queryCurrentUser(operator.database);
|
||||
if (!currentUser) {
|
||||
return {error: 'Cannot get the current user'};
|
||||
}
|
||||
const created = await client.createGroupChannel(userIds);
|
||||
|
||||
// Check the channel previous existency: if the channel already have
|
||||
// posts is because it existed before.
|
||||
if (created.total_msg_count > 0) {
|
||||
return {data: created};
|
||||
}
|
||||
|
||||
const preferences = await queryPreferencesByCategoryAndName(operator.database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT);
|
||||
const system = await queryCommonSystemValues(operator.database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], system.config, system.license);
|
||||
const {directChannels} = await fetchMissingSidebarInfo(serverUrl, [created], currentUser.locale, teammateDisplayNameSetting, currentUser.id, true);
|
||||
|
||||
const member = {
|
||||
channel_id: created.id,
|
||||
user_id: '',
|
||||
roles: `${General.CHANNEL_USER_ROLE}`,
|
||||
last_viewed_at: 0,
|
||||
msg_count: 0,
|
||||
mention_count: 0,
|
||||
msg_count_root: 0,
|
||||
mention_count_root: 0,
|
||||
notify_props: {desktop: 'default' as const, mark_unread: 'all' as const},
|
||||
last_update_at: created.create_at,
|
||||
};
|
||||
|
||||
const members = userIds.map((id) => {
|
||||
return {...member, user_id: id};
|
||||
});
|
||||
|
||||
if (directChannels?.length) {
|
||||
const channelPromises = await prepareMyChannelsForTeam(operator, '', directChannels, members);
|
||||
if (channelPromises) {
|
||||
const channelModels = await Promise.all(channelPromises);
|
||||
const flattenedChannelModels = channelModels.flat();
|
||||
if (flattenedChannelModels.length) {
|
||||
operator.batchRecords(flattenedChannelModels);
|
||||
}
|
||||
}
|
||||
}
|
||||
fetchRolesIfNeeded(serverUrl, member.roles.split(' '));
|
||||
return {data: created};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
export const fetchSharedChannels = async (serverUrl: string, teamId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE) => {
|
||||
let client: Client;
|
||||
try {
|
||||
@@ -599,6 +756,27 @@ export const fetchSharedChannels = async (serverUrl: string, teamId: string, pag
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const makeGroupChannel = async (serverUrl: string, userIds: string[], shouldSwitchToChannel = true) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const currentUserId = await queryCurrentUserId(operator.database);
|
||||
const result = await createGroupChannel(serverUrl, [currentUserId, ...userIds]);
|
||||
const channel = result.data;
|
||||
|
||||
if (channel && shouldSwitchToChannel) {
|
||||
switchToChannelById(serverUrl, channel.id);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
export async function getChannelMemberCountsByGroup(serverUrl: string, channelId: string, includeTimezones: boolean) {
|
||||
let client: Client;
|
||||
try {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {chunk} from 'lodash';
|
||||
import {updateChannelsDisplayName} from '@actions/local/channel';
|
||||
import {updateRecentCustomStatuses, updateLocalUser} from '@actions/local/user';
|
||||
import {fetchRolesIfNeeded} from '@actions/remote/role';
|
||||
import {removeUserFromList} from '@app/utils/user';
|
||||
import {Database, General} from '@constants';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
@@ -248,7 +249,7 @@ export const fetchUsersByIds = async (serverUrl: string, userIds: string[], fetc
|
||||
return {error};
|
||||
}
|
||||
if (!userIds.length) {
|
||||
return {users: []};
|
||||
return {users: [], existingUsers: []};
|
||||
}
|
||||
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
@@ -257,9 +258,15 @@ export const fetchUsersByIds = async (serverUrl: string, userIds: string[], fetc
|
||||
}
|
||||
|
||||
try {
|
||||
const currentUserId = await queryCurrentUserId(operator.database);
|
||||
const exisingUsers = await queryUsersById(operator.database, userIds);
|
||||
const usersToLoad = userIds.filter((id) => (id !== currentUserId && !exisingUsers.find((u) => u.id === id)));
|
||||
const currentUser = await queryCurrentUser(operator.database);
|
||||
const existingUsers = await queryUsersById(operator.database, userIds);
|
||||
if (userIds.includes(currentUser!.id)) {
|
||||
existingUsers.push(currentUser!);
|
||||
}
|
||||
const usersToLoad = new Set(userIds.filter((id) => (!existingUsers.find((u) => u.id === id))));
|
||||
if (usersToLoad.size === 0) {
|
||||
return {users: [], existingUsers};
|
||||
}
|
||||
const users = await client.getProfilesByIds([...new Set(usersToLoad)]);
|
||||
if (!fetchOnly) {
|
||||
await operator.handleUsers({
|
||||
@@ -268,7 +275,7 @@ export const fetchUsersByIds = async (serverUrl: string, userIds: string[], fetc
|
||||
});
|
||||
}
|
||||
|
||||
return {users};
|
||||
return {users, existingUsers};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
@@ -311,6 +318,103 @@ export const fetchUsersByUsernames = async (serverUrl: string, usernames: string
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchProfiles = async (serverUrl: string, page = 0, perPage: number = General.PROFILE_CHUNK_SIZE, options: any = {}, fetchOnly = false) => {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const users = await client.getProfiles(page, perPage, options);
|
||||
|
||||
if (!fetchOnly) {
|
||||
const currentUserId = await queryCurrentUserId(operator.database);
|
||||
const toStore = removeUserFromList(currentUserId, users);
|
||||
await operator.handleUsers({
|
||||
users: toStore,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
}
|
||||
|
||||
return {users};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchProfilesInTeam = async (serverUrl: string, teamId: string, page = 0, perPage: number = General.PROFILE_CHUNK_SIZE, sort = '', options: any = {}, fetchOnly = false) => {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const users = await client.getProfilesInTeam(teamId, page, perPage, sort, options);
|
||||
|
||||
if (!fetchOnly) {
|
||||
const currentUserId = await queryCurrentUserId(operator.database);
|
||||
const toStore = removeUserFromList(currentUserId, users);
|
||||
|
||||
await operator.handleUsers({
|
||||
users: toStore,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
}
|
||||
|
||||
return {users};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const searchProfiles = async (serverUrl: string, term: string, options: any = {}, fetchOnly = false) => {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const currentUserId = await queryCurrentUserId(operator.database);
|
||||
const users = await client.searchUsers(term, options);
|
||||
|
||||
if (!fetchOnly) {
|
||||
const toStore = removeUserFromList(currentUserId, users);
|
||||
await operator.handleUsers({
|
||||
users: toStore,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
}
|
||||
|
||||
return {data: users};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchMissingProfilesByIds = async (serverUrl: string, userIds: string[]) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
|
||||
@@ -13,11 +13,10 @@ import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
import WebsocketManager from '@init/websocket_manager';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {queryCommonSystemValues} from '@queries/servers/system';
|
||||
import {queryCurrentUser, queryUserById} from '@queries/servers/user';
|
||||
import {queryCurrentUser} from '@queries/servers/user';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
const {SERVER: {CHANNEL, CHANNEL_MEMBERSHIP}} = MM_TABLES;
|
||||
|
||||
@@ -94,11 +93,9 @@ export async function handleUserTypingEvent(serverUrl: string, msg: WebSocketMes
|
||||
|
||||
const {config, license} = await queryCommonSystemValues(database);
|
||||
|
||||
let user: UserModel | UserProfile | undefined = await queryUserById(database, msg.data.user_id);
|
||||
if (!user) {
|
||||
const {users} = await fetchUsersByIds(serverUrl, [msg.data.user_id]);
|
||||
user = users?.[0];
|
||||
}
|
||||
const {users, existingUsers} = await fetchUsersByIds(serverUrl, [msg.data.user_id]);
|
||||
const user = users?.[0] || existingUsers?.[0];
|
||||
|
||||
const namePreference = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(namePreference, config, license);
|
||||
const currentUser = await queryCurrentUser(database);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import React, {useCallback, useEffect} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Text, View} from 'react-native';
|
||||
import {Platform, Text, View} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
@@ -101,7 +101,7 @@ const ChannelListHeader = ({canCreateChannels, canJoinChannels, displayName, ico
|
||||
bottomSheet({
|
||||
closeButtonId,
|
||||
renderContent,
|
||||
snapPoints: [(items * ITEM_HEIGHT) + (insets.bottom * 2), 10],
|
||||
snapPoints: [((items + Platform.select({android: 1, default: 0})) * ITEM_HEIGHT) + (insets.bottom * 2), 10],
|
||||
theme,
|
||||
title: intl.formatMessage({id: 'home.header.plus_menu', defaultMessage: 'Options'}),
|
||||
});
|
||||
|
||||
@@ -36,7 +36,13 @@ const PlusMenuList = ({canCreateChannels, canJoinChannels}: Props) => {
|
||||
}, [intl, theme]);
|
||||
|
||||
const openDirectMessage = useCallback(async () => {
|
||||
// To be added
|
||||
await dismissBottomSheet();
|
||||
|
||||
const title = intl.formatMessage({id: 'create_direct_message.title', defaultMessage: 'Create Direct Message'});
|
||||
const closeButton = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
|
||||
showModal(Screens.CREATE_DIRECT_MESSAGE, title, {
|
||||
closeButton,
|
||||
});
|
||||
}, [intl, theme]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,7 +15,7 @@ import type {Client} from '@client/rest';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
author?: UserModel;
|
||||
author?: UserModel | UserProfile;
|
||||
iconSize?: number;
|
||||
size: number;
|
||||
source?: Source | string;
|
||||
@@ -58,7 +58,8 @@ const Image = ({author, iconSize, size, source}: Props) => {
|
||||
}
|
||||
|
||||
if (author && client) {
|
||||
const pictureUrl = client.getProfilePictureUrl(author.id, author.lastPictureUpdate);
|
||||
const lastPictureUpdate = ('lastPictureUpdate' in author) ? author.lastPictureUpdate : author.last_picture_update;
|
||||
const pictureUrl = client.getProfilePictureUrl(author.id, lastPictureUpdate);
|
||||
const imgSource = source ?? {uri: `${serverUrl}${pictureUrl}`};
|
||||
return (
|
||||
<FastImage
|
||||
|
||||
@@ -21,7 +21,7 @@ const STATUS_BUFFER = Platform.select({
|
||||
});
|
||||
|
||||
type ProfilePictureProps = {
|
||||
author?: UserModel;
|
||||
author?: UserModel | UserProfile;
|
||||
iconSize?: number;
|
||||
showStatus?: boolean;
|
||||
size: number;
|
||||
@@ -73,9 +73,10 @@ const ProfilePicture = ({
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const buffer = showStatus ? STATUS_BUFFER || 0 : 0;
|
||||
const isBot = author && (('isBot' in author) ? author.isBot : author.is_bot);
|
||||
|
||||
useEffect(() => {
|
||||
if (author && !author.status && showStatus) {
|
||||
if (!isBot && author && !author.status && showStatus) {
|
||||
fetchStatusInBatch(serverUrl, author.id);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -10,7 +10,7 @@ import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
author?: UserModel;
|
||||
author?: UserModel | UserProfile;
|
||||
statusSize: number;
|
||||
statusStyle?: StyleProp<ViewProps>;
|
||||
theme: Theme;
|
||||
@@ -39,8 +39,8 @@ const Status = ({author, statusSize, statusStyle, theme}: Props) => {
|
||||
statusStyle,
|
||||
{borderRadius: statusSize / 2},
|
||||
]), [statusStyle]);
|
||||
|
||||
if (author?.status && !author.isBot) {
|
||||
const isBot = author && (('isBot' in author) ? author.isBot : author.is_bot);
|
||||
if (author?.status && !isBot) {
|
||||
return (
|
||||
<View
|
||||
style={containerStyle}
|
||||
|
||||
218
app/components/user_list_row/index.tsx
Normal file
218
app/components/user_list_row/index.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
// 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 {
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
import {BotTag, GuestTag} from '@components/tag';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
import {displayUsername, isGuest} from '@utils/user';
|
||||
|
||||
import TouchableWithFeedback from '../touchable_with_feedback';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
isMyUser: boolean;
|
||||
user: UserProfile;
|
||||
teammateNameDisplay: string;
|
||||
testID: string;
|
||||
onPress?: (user: UserProfile) => void;
|
||||
selectable: boolean;
|
||||
selected: boolean;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 15,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
profileContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
textContainer: {
|
||||
marginLeft: 10,
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
},
|
||||
displayName: {
|
||||
fontSize: 15,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
username: {
|
||||
fontSize: 15,
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
indicatorContainer: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
deactivated: {
|
||||
marginTop: 2,
|
||||
fontSize: 12,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
sharedUserIcon: {
|
||||
alignSelf: 'center',
|
||||
opacity: 0.75,
|
||||
},
|
||||
selector: {
|
||||
height: 28,
|
||||
width: 28,
|
||||
borderRadius: 14,
|
||||
borderWidth: 3,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.32),
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
selectorContainer: {
|
||||
height: 50,
|
||||
paddingRight: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
selectorDisabled: {
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||
},
|
||||
selectorFilled: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
borderWidth: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default function UserListRow({
|
||||
id,
|
||||
isMyUser,
|
||||
user,
|
||||
teammateNameDisplay,
|
||||
testID,
|
||||
onPress,
|
||||
selectable,
|
||||
selected,
|
||||
enabled,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
const intl = useIntl();
|
||||
const {formatMessage} = intl;
|
||||
|
||||
const {username} = user;
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
if (onPress) {
|
||||
onPress(user);
|
||||
}
|
||||
}, [onPress, user]);
|
||||
|
||||
const iconStyle = useMemo(() => {
|
||||
return [style.selector, (selected && style.selectorFilled), (!enabled && style.selectorDisabled)];
|
||||
}, [style, selected, enabled]);
|
||||
|
||||
const Icon = () => {
|
||||
return (
|
||||
<View style={style.selectorContainer}>
|
||||
<View style={iconStyle}>
|
||||
{selected &&
|
||||
<CompassIcon
|
||||
name='check'
|
||||
size={24}
|
||||
color={theme.sidebarText}
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
let usernameDisplay = `@${username}`;
|
||||
if (isMyUser) {
|
||||
usernameDisplay = formatMessage({
|
||||
id: 'mobile.create_direct_message.you',
|
||||
defaultMessage: '@{username} - you',
|
||||
}, {username});
|
||||
}
|
||||
|
||||
const teammateDisplay = displayUsername(user, intl.locale, teammateNameDisplay);
|
||||
const showTeammateDisplay = teammateDisplay !== username;
|
||||
|
||||
const itemTestID = `${testID}.${id}`;
|
||||
const displayUsernameTestID = `${testID}.display_username`;
|
||||
const profilePictureTestID = `${itemTestID}.profile_picture`;
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={handlePress}
|
||||
>
|
||||
<View style={style.container}>
|
||||
<View style={style.profileContainer}>
|
||||
<ProfilePicture
|
||||
author={user}
|
||||
size={32}
|
||||
iconSize={24}
|
||||
testID={profilePictureTestID}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={style.textContainer}
|
||||
testID={itemTestID}
|
||||
>
|
||||
<View>
|
||||
<View style={style.indicatorContainer}>
|
||||
<Text
|
||||
style={style.username}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
testID={displayUsernameTestID}
|
||||
>
|
||||
{usernameDisplay}
|
||||
</Text>
|
||||
<BotTag
|
||||
show={Boolean(user.is_bot)}
|
||||
/>
|
||||
<GuestTag
|
||||
show={isGuest(user.roles)}
|
||||
/>
|
||||
</View>
|
||||
</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>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,4 +32,9 @@ export default {
|
||||
DISABLED: 'disabled',
|
||||
DEFAULT_ON: 'default_on',
|
||||
DEFAULT_OFF: 'default_off',
|
||||
PROFILE_CHUNK_SIZE: 100,
|
||||
SEARCH_TIMEOUT_MILLISECONDS: 100,
|
||||
AUTOCOMPLETE_SPLIT_CHARACTERS: ['.', '-', '_'],
|
||||
CHANNEL_USER_ROLE: 'channel_user',
|
||||
RESTRICT_DIRECT_MESSAGE_ANY: 'any',
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ export const IN_APP_NOTIFICATION = 'InAppNotification';
|
||||
export const LOGIN = 'Login';
|
||||
export const MENTIONS = 'Mentions';
|
||||
export const MFA = 'MFA';
|
||||
export const CREATE_DIRECT_MESSAGE = 'CreateDirectMessage';
|
||||
export const PERMALINK = 'Permalink';
|
||||
export const SEARCH = 'Search';
|
||||
export const SERVER = 'Server';
|
||||
@@ -55,6 +56,7 @@ export default {
|
||||
LOGIN,
|
||||
MENTIONS,
|
||||
MFA,
|
||||
CREATE_DIRECT_MESSAGE,
|
||||
PERMALINK,
|
||||
SEARCH,
|
||||
SERVER,
|
||||
|
||||
407
app/screens/create_direct_message/create_direct_message.tsx
Normal file
407
app/screens/create_direct_message/create_direct_message.tsx
Normal file
@@ -0,0 +1,407 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Keyboard, View} from 'react-native';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {makeDirectChannel, makeGroupChannel} from '@actions/remote/channel';
|
||||
import {fetchProfiles, fetchProfilesInTeam, searchProfiles} from '@actions/remote/user';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import Loading from '@components/loading';
|
||||
import SearchBar from '@components/search_bar';
|
||||
import {General} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {debounce} from '@helpers/api/general';
|
||||
import {t} from '@i18n';
|
||||
import {dismissModal, setButtons} from '@screens/navigation';
|
||||
import {alertErrorWithFallback} from '@utils/draft';
|
||||
import {
|
||||
changeOpacity,
|
||||
makeStyleSheetFromTheme,
|
||||
getKeyboardAppearanceFromTheme,
|
||||
} from '@utils/theme';
|
||||
import {displayUsername, filterProfilesMatchingTerm} from '@utils/user';
|
||||
|
||||
import SelectedUsers from './selected_users';
|
||||
import UserList from './user_list';
|
||||
|
||||
const START_BUTTON = 'start-conversation';
|
||||
const CLOSE_BUTTON = 'close-dms';
|
||||
|
||||
type Props = {
|
||||
componentId: string;
|
||||
currentTeamId: string;
|
||||
currentUserId: string;
|
||||
restrictDirectMessage: boolean;
|
||||
teammateNameDisplay: string;
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
Keyboard.dismiss();
|
||||
dismissModal();
|
||||
};
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
searchBar: {
|
||||
marginHorizontal: 12,
|
||||
borderRadius: 8,
|
||||
marginTop: 12,
|
||||
marginBottom: 12,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
},
|
||||
searchBarInput: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
loadingContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
height: 70,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
},
|
||||
noResultContainer: {
|
||||
flexGrow: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
noResultText: {
|
||||
fontSize: 26,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default function CreateDirectMessage({
|
||||
componentId,
|
||||
currentTeamId,
|
||||
currentUserId,
|
||||
restrictDirectMessage,
|
||||
teammateNameDisplay,
|
||||
}: Props) {
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
const intl = useIntl();
|
||||
const {formatMessage} = intl;
|
||||
|
||||
const searchTimeoutId = useRef<NodeJS.Timeout | null>(null);
|
||||
const next = useRef(true);
|
||||
const page = useRef(-1);
|
||||
const mounted = useRef(false);
|
||||
|
||||
const [profiles, setProfiles] = useState<UserProfile[]>([]);
|
||||
const [searchResults, setSearchResults] = useState<UserProfile[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [term, setTerm] = useState('');
|
||||
const [startingConversation, setStartingConversation] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<{[id: string]: UserProfile}>({});
|
||||
const selectedCount = Object.keys(selectedIds).length;
|
||||
|
||||
const isSearch = Boolean(term);
|
||||
|
||||
const addToProfiles = useRef((newProfiles: UserProfile[]) => setProfiles([...profiles, ...newProfiles]));
|
||||
useEffect(() => {
|
||||
addToProfiles.current = (newProfiles: UserProfile[]) => setProfiles([...profiles, ...newProfiles]);
|
||||
}, [profiles]);
|
||||
|
||||
const loadedProfiles = ({users}: {users?: UserProfile[]}) => {
|
||||
if (mounted.current) {
|
||||
if (users && !users.length) {
|
||||
next.current = false;
|
||||
}
|
||||
|
||||
page.current += 1;
|
||||
setLoading(false);
|
||||
if (users && users.length) {
|
||||
addToProfiles.current(users);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const clearSearch = useCallback(() => {
|
||||
setTerm('');
|
||||
setSearchResults([]);
|
||||
}, []);
|
||||
|
||||
const getProfiles = useCallback(debounce(() => {
|
||||
if (next.current && !loading && !term && mounted.current) {
|
||||
setLoading(true);
|
||||
if (restrictDirectMessage) {
|
||||
fetchProfilesInTeam(serverUrl, currentTeamId, page.current + 1, General.PROFILE_CHUNK_SIZE).then(loadedProfiles);
|
||||
} else {
|
||||
fetchProfiles(serverUrl, page.current + 1, General.PROFILE_CHUNK_SIZE).then(loadedProfiles);
|
||||
}
|
||||
}
|
||||
}, 100), [loading, isSearch, restrictDirectMessage, serverUrl, currentTeamId]);
|
||||
|
||||
const handleRemoveProfile = useCallback((id: string) => {
|
||||
const newSelectedIds = Object.assign({}, selectedIds);
|
||||
|
||||
Reflect.deleteProperty(newSelectedIds, id);
|
||||
|
||||
setSelectedIds(newSelectedIds);
|
||||
}, [selectedIds]);
|
||||
|
||||
const createDirectChannel = useCallback(async (id: string): Promise<boolean> => {
|
||||
const user = profiles.find((u) => u.id === id);
|
||||
|
||||
const displayName = displayUsername(user, intl.locale, teammateNameDisplay);
|
||||
|
||||
const result = await makeDirectChannel(serverUrl, id, displayName);
|
||||
|
||||
if (result.error) {
|
||||
alertErrorWithFallback(
|
||||
intl,
|
||||
result.error,
|
||||
{
|
||||
id: 'mobile.open_dm.error',
|
||||
defaultMessage: "We couldn't open a direct message with {displayName}. Please check your connection and try again.",
|
||||
},
|
||||
{
|
||||
displayName,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return !result.error;
|
||||
}, [profiles, intl.locale, teammateNameDisplay, serverUrl]);
|
||||
|
||||
const createGroupChannel = useCallback(async (ids: string[]): Promise<boolean> => {
|
||||
const result = await makeGroupChannel(serverUrl, ids);
|
||||
|
||||
if (result.error) {
|
||||
alertErrorWithFallback(
|
||||
intl,
|
||||
result.error,
|
||||
{
|
||||
id: t('mobile.open_gm.error'),
|
||||
defaultMessage: "We couldn't open a group message with those users. Please check your connection and try again.",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return !result.error;
|
||||
}, [serverUrl]);
|
||||
|
||||
const startConversation = useCallback(async (selectedId?: {[id: string]: boolean}) => {
|
||||
if (startingConversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
setStartingConversation(true);
|
||||
|
||||
const idsToUse = selectedId ? Object.keys(selectedId) : Object.keys(selectedIds);
|
||||
let success;
|
||||
if (idsToUse.length === 0) {
|
||||
success = false;
|
||||
} else if (idsToUse.length > 1) {
|
||||
success = await createGroupChannel(idsToUse);
|
||||
} else {
|
||||
success = await createDirectChannel(idsToUse[0]);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
close();
|
||||
} else {
|
||||
setStartingConversation(false);
|
||||
}
|
||||
}, [startingConversation, selectedIds, createGroupChannel, createDirectChannel]);
|
||||
|
||||
const handleSelectProfile = useCallback((user: UserProfile) => {
|
||||
if (selectedIds[user.id]) {
|
||||
handleRemoveProfile(user.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.id === currentUserId) {
|
||||
const selectedId = {
|
||||
[currentUserId]: true,
|
||||
};
|
||||
|
||||
startConversation(selectedId);
|
||||
} else {
|
||||
const wasSelected = selectedIds[user.id];
|
||||
|
||||
if (!wasSelected && selectedCount >= General.MAX_USERS_IN_GM - 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newSelectedIds = Object.assign({}, selectedIds);
|
||||
if (!wasSelected) {
|
||||
newSelectedIds[user.id] = user;
|
||||
}
|
||||
|
||||
setSelectedIds(newSelectedIds);
|
||||
|
||||
clearSearch();
|
||||
}
|
||||
}, [selectedIds, currentUserId, handleRemoveProfile, startConversation, clearSearch]);
|
||||
|
||||
const searchUsers = useCallback(async (searchTerm: string) => {
|
||||
const lowerCasedTerm = searchTerm.toLowerCase();
|
||||
setLoading(true);
|
||||
let results;
|
||||
|
||||
if (restrictDirectMessage) {
|
||||
results = await searchProfiles(serverUrl, lowerCasedTerm);
|
||||
} else {
|
||||
results = await searchProfiles(serverUrl, lowerCasedTerm, {team_id: currentTeamId});
|
||||
}
|
||||
|
||||
let data: UserProfile[] = [];
|
||||
if (results.data) {
|
||||
data = results.data;
|
||||
}
|
||||
|
||||
setSearchResults(data);
|
||||
setLoading(false);
|
||||
}, [restrictDirectMessage, serverUrl, currentTeamId]);
|
||||
|
||||
const onSearch = useCallback((text: string) => {
|
||||
if (text) {
|
||||
setTerm(text);
|
||||
if (searchTimeoutId.current) {
|
||||
clearTimeout(searchTimeoutId.current);
|
||||
}
|
||||
|
||||
searchTimeoutId.current = setTimeout(() => {
|
||||
searchUsers(text);
|
||||
}, General.SEARCH_TIMEOUT_MILLISECONDS);
|
||||
} else {
|
||||
clearSearch();
|
||||
}
|
||||
}, [searchUsers, clearSearch]);
|
||||
|
||||
const updateNavigationButtons = useCallback(async (startEnabled: boolean) => {
|
||||
const closeIcon = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
|
||||
setButtons(componentId, {
|
||||
leftButtons: [{
|
||||
id: CLOSE_BUTTON,
|
||||
icon: closeIcon,
|
||||
testID: 'close.more_direct_messages.button',
|
||||
}],
|
||||
rightButtons: [{
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
id: START_BUTTON,
|
||||
text: formatMessage({id: 'mobile.create_direct_message.start', defaultMessage: 'Start'}),
|
||||
showAsAction: 'always',
|
||||
enabled: startEnabled,
|
||||
testID: 'more_direct_messages.start.button',
|
||||
}],
|
||||
});
|
||||
}, [intl.locale, theme]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = Navigation.events().registerComponentListener({navigationButtonPressed: ({buttonId}) => {
|
||||
if (buttonId === START_BUTTON) {
|
||||
startConversation();
|
||||
} else if (buttonId === CLOSE_BUTTON) {
|
||||
close();
|
||||
}
|
||||
}}, componentId);
|
||||
return () => {
|
||||
unsubscribe.remove();
|
||||
};
|
||||
}, [startConversation]);
|
||||
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
updateNavigationButtons(false);
|
||||
getProfiles();
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const canStart = selectedCount > 0 && !startingConversation;
|
||||
updateNavigationButtons(canStart);
|
||||
}, [selectedCount > 0, startingConversation, updateNavigationButtons]);
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (term) {
|
||||
const exactMatches: UserProfile[] = [];
|
||||
const filterByTerm = (p: UserProfile) => {
|
||||
if (selectedCount > 0 && p.id === currentUserId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (p.username === term || p.username.startsWith(term)) {
|
||||
exactMatches.push(p);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const results = filterProfilesMatchingTerm(searchResults, term).filter(filterByTerm);
|
||||
return [...exactMatches, ...results];
|
||||
}
|
||||
return profiles;
|
||||
}, [term, isSearch && selectedCount, isSearch && searchResults, profiles]);
|
||||
|
||||
if (startingConversation) {
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<Loading color={theme.centerChannelColor}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={style.container}>
|
||||
<View style={style.searchBar}>
|
||||
<SearchBar
|
||||
testID='more_direct_messages.search_bar'
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
backgroundColor='transparent'
|
||||
inputHeight={33}
|
||||
inputStyle={style.searchBarInput}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
titleCancelColor={theme.centerChannelColor}
|
||||
onChangeText={onSearch}
|
||||
onSearchButtonPress={onSearch}
|
||||
onCancelButtonPress={clearSearch}
|
||||
autoCapitalize='none'
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
value={term}
|
||||
/>
|
||||
</View>
|
||||
{selectedCount > 0 &&
|
||||
<SelectedUsers
|
||||
selectedIds={selectedIds}
|
||||
warnCount={5}
|
||||
maxCount={7}
|
||||
onRemove={handleRemoveProfile}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
/>
|
||||
}
|
||||
<UserList
|
||||
currentUserId={currentUserId}
|
||||
handleSelectProfile={handleSelectProfile}
|
||||
isSearch={isSearch}
|
||||
loading={loading}
|
||||
profiles={data}
|
||||
selectedIds={selectedIds}
|
||||
showNoResults={!loading && page.current !== -1}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
fetchMore={getProfiles}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
52
app/screens/create_direct_message/index.ts
Normal file
52
app/screens/create_direct_message/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {combineLatest, of as of$} from 'rxjs';
|
||||
import {map, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {General, Preferences} from '@constants';
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
|
||||
import CreateDirectMessage from './create_direct_message';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type PreferenceModel from '@typings/database/models/servers/preference';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
|
||||
const {SERVER: {SYSTEM, PREFERENCE}} = MM_TABLES;
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const config = database.get<SystemModel>(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CONFIG);
|
||||
const license = database.get<SystemModel>(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.LICENSE);
|
||||
const preferences = database.get<PreferenceModel>(PREFERENCE).query(Q.where('category', Preferences.CATEGORY_DISPLAY_SETTINGS)).observe();
|
||||
const currentUserId = database.get<SystemModel>(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_USER_ID).pipe(
|
||||
switchMap(({value}) => of$(value)),
|
||||
);
|
||||
const currentTeamId = database.get<SystemModel>(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID).pipe(
|
||||
switchMap(({value}) => of$(value)),
|
||||
);
|
||||
|
||||
const teammateNameDisplay = combineLatest([config, license, preferences]).pipe(
|
||||
map(
|
||||
([{value: cfg}, {value: lcs}, prefs]) => getTeammateNameDisplaySetting(prefs, cfg, lcs),
|
||||
),
|
||||
);
|
||||
|
||||
const restrictDirectMessage = config.pipe(
|
||||
switchMap(({value: cfg}) => of$(cfg.RestrictDirectMessage !== General.RESTRICT_DIRECT_MESSAGE_ANY)),
|
||||
);
|
||||
|
||||
return {
|
||||
teammateNameDisplay,
|
||||
currentUserId,
|
||||
restrictDirectMessage,
|
||||
currentTeamId,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(CreateDirectMessage));
|
||||
|
||||
138
app/screens/create_direct_message/selected_users/index.tsx
Normal file
138
app/screens/create_direct_message/selected_users/index.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import SelectedUser from './selected_user';
|
||||
|
||||
type Props = {
|
||||
|
||||
/*
|
||||
* An object mapping user ids to a falsey value indicating whether or not they've been selected.
|
||||
*/
|
||||
selectedIds: {[id: string]: UserProfile};
|
||||
|
||||
/*
|
||||
* How to display the names of users.
|
||||
*/
|
||||
teammateNameDisplay: string;
|
||||
|
||||
/*
|
||||
* The number of users that will be selected when we start to display a message indicating
|
||||
* the remaining number of users that can be selected.
|
||||
*/
|
||||
warnCount: number;
|
||||
|
||||
/*
|
||||
* The maximum number of users that can be selected.
|
||||
*/
|
||||
maxCount: number;
|
||||
|
||||
/*
|
||||
* A handler function that will deselect a user when clicked on.
|
||||
*/
|
||||
onRemove: (id: string) => void;
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
marginHorizontal: 12,
|
||||
},
|
||||
users: {
|
||||
alignItems: 'flex-start',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
message: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
fontSize: 12,
|
||||
marginRight: 5,
|
||||
marginTop: 10,
|
||||
marginBottom: 2,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default function SelectedUsers({
|
||||
selectedIds,
|
||||
teammateNameDisplay,
|
||||
warnCount,
|
||||
maxCount,
|
||||
onRemove,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
const users = useMemo(() => {
|
||||
const u = [];
|
||||
for (const id of Object.keys(selectedIds)) {
|
||||
if (!selectedIds[id]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
u.push(
|
||||
<SelectedUser
|
||||
key={id}
|
||||
user={selectedIds[id]}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
onRemove={onRemove}
|
||||
testID='more_direct_messages.selected_user'
|
||||
/>,
|
||||
);
|
||||
}
|
||||
return u;
|
||||
}, [selectedIds, teammateNameDisplay, onRemove]);
|
||||
|
||||
const showWarn = users.length >= warnCount && users.length < maxCount;
|
||||
|
||||
const message = useMemo(() => {
|
||||
if (users.length >= maxCount) {
|
||||
return (
|
||||
<FormattedText
|
||||
style={style.message}
|
||||
id='mobile.create_direct_message.cannot_add_more'
|
||||
defaultMessage='You cannot add more users'
|
||||
/>
|
||||
);
|
||||
} else if (users.length >= warnCount) {
|
||||
const remaining = maxCount - users.length;
|
||||
if (remaining === 1) {
|
||||
return (
|
||||
<FormattedText
|
||||
style={style.message}
|
||||
id='mobile.create_direct_message.one_more'
|
||||
defaultMessage='You can add 1 more user'
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FormattedText
|
||||
style={style.message}
|
||||
id='mobile.create_direct_message.add_more'
|
||||
defaultMessage='You can add {remaining, number} more users'
|
||||
values={{
|
||||
remaining,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [users.length >= maxCount, showWarn && users.length, theme, maxCount]);
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<View style={style.users}>
|
||||
{users}
|
||||
</View>
|
||||
{message}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import {typography} from '@app/utils/typography';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
type Props = {
|
||||
|
||||
/*
|
||||
* How to display the names of users.
|
||||
*/
|
||||
teammateNameDisplay: string;
|
||||
|
||||
/*
|
||||
* The user that this component represents.
|
||||
*/
|
||||
user: UserProfile;
|
||||
|
||||
/*
|
||||
* A handler function that will deselect a user when clicked on.
|
||||
*/
|
||||
onRemove: (id: string) => void;
|
||||
|
||||
/*
|
||||
* The test ID.
|
||||
*/
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
marginBottom: 8,
|
||||
marginRight: 10,
|
||||
paddingLeft: 12,
|
||||
paddingVertical: 8,
|
||||
paddingRight: 7,
|
||||
},
|
||||
remove: {
|
||||
marginLeft: 7,
|
||||
},
|
||||
text: {
|
||||
color: theme.centerChannelColor,
|
||||
textAlignVertical: 'center',
|
||||
height: 32,
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default function SelectedUser({
|
||||
teammateNameDisplay,
|
||||
user,
|
||||
onRemove,
|
||||
testID,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
const intl = useIntl();
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
onRemove(user.id);
|
||||
}, [onRemove, user.id]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={style.container}
|
||||
testID={`${testID}.${user.id}`}
|
||||
>
|
||||
<Text
|
||||
style={style.text}
|
||||
testID={`${testID}.${user.id}.display_username`}
|
||||
>
|
||||
{displayUsername(user, intl.locale, teammateNameDisplay)}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={style.remove}
|
||||
onPress={onPress}
|
||||
testID={`${testID}.${user.id}.remove.button`}
|
||||
>
|
||||
<CompassIcon
|
||||
name='close-circle'
|
||||
size={17}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.32)}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
275
app/screens/create_direct_message/user_list.tsx
Normal file
275
app/screens/create_direct_message/user_list.tsx
Normal file
@@ -0,0 +1,275 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {FlatList, Keyboard, ListRenderItemInfo, Platform, SectionList, SectionListData, Text, View} from 'react-native';
|
||||
|
||||
import {typography} from '@app/utils/typography';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import UserListRow from '@components/user_list_row';
|
||||
import {General} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {
|
||||
changeOpacity,
|
||||
makeStyleSheetFromTheme,
|
||||
} from '@utils/theme';
|
||||
|
||||
const INITIAL_BATCH_TO_RENDER = 15;
|
||||
const SCROLL_EVENT_THROTTLE = 60;
|
||||
|
||||
const keyboardDismissProp = Platform.select({
|
||||
android: {
|
||||
onScrollBeginDrag: Keyboard.dismiss,
|
||||
},
|
||||
ios: {
|
||||
keyboardDismissMode: 'on-drag' as const,
|
||||
},
|
||||
});
|
||||
|
||||
const keyExtractor = (item: UserProfile) => {
|
||||
return item.id;
|
||||
};
|
||||
|
||||
const sectionKeyExtractor = (profile: UserProfile) => {
|
||||
// Group items alphabetically by first letter of username
|
||||
return profile.username[0].toUpperCase();
|
||||
};
|
||||
|
||||
export function createProfilesSections(profiles: UserProfile[]) {
|
||||
const sections: {[key: string]: UserProfile[]} = {};
|
||||
const sectionKeys: string[] = [];
|
||||
for (const profile of profiles) {
|
||||
const sectionKey = sectionKeyExtractor(profile);
|
||||
|
||||
if (!sections[sectionKey]) {
|
||||
sections[sectionKey] = [];
|
||||
sectionKeys.push(sectionKey);
|
||||
}
|
||||
|
||||
sections[sectionKey].push(profile);
|
||||
}
|
||||
|
||||
sectionKeys.sort();
|
||||
|
||||
return sectionKeys.map((sectionKey) => {
|
||||
return {
|
||||
id: sectionKey,
|
||||
data: sections[sectionKey],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
list: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flex: 1,
|
||||
...Platform.select({
|
||||
android: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
}),
|
||||
},
|
||||
container: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
},
|
||||
listView: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
...Platform.select({
|
||||
android: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
}),
|
||||
},
|
||||
loadingText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
},
|
||||
searching: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
},
|
||||
sectionContainer: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
paddingLeft: 10,
|
||||
paddingVertical: 2,
|
||||
height: 28,
|
||||
},
|
||||
sectionWrapper: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
sectionText: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 300, 'SemiBold'),
|
||||
},
|
||||
noResultContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
noResultText: {
|
||||
fontSize: 26,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type Props = {
|
||||
profiles: UserProfile[];
|
||||
currentUserId: string;
|
||||
teammateNameDisplay: string;
|
||||
handleSelectProfile: (user: UserProfile) => void;
|
||||
fetchMore: () => void;
|
||||
loading: boolean;
|
||||
showNoResults: boolean;
|
||||
selectedIds: {[id: string]: UserProfile};
|
||||
testID?: string;
|
||||
isSearch: boolean;
|
||||
}
|
||||
|
||||
export default function UserList({
|
||||
profiles,
|
||||
selectedIds,
|
||||
currentUserId,
|
||||
teammateNameDisplay,
|
||||
handleSelectProfile,
|
||||
fetchMore,
|
||||
loading,
|
||||
showNoResults,
|
||||
isSearch,
|
||||
testID,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
const renderItem = useCallback(({item}: 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;
|
||||
|
||||
return (
|
||||
<UserListRow
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
isMyUser={currentUserId === item.id}
|
||||
onPress={handleSelectProfile}
|
||||
selectable={canAdd}
|
||||
selected={selected}
|
||||
enabled={canAdd}
|
||||
testID='more_direct_messages.user_list.user_item'
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
user={item}
|
||||
/>
|
||||
);
|
||||
}, [selectedIds, currentUserId, handleSelectProfile, teammateNameDisplay]);
|
||||
|
||||
const renderLoading = useCallback(() => {
|
||||
if (!loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.loadingContainer}>
|
||||
<FormattedText
|
||||
id='mobile.loading_members'
|
||||
defaultMessage='Loading Members...'
|
||||
style={style.loadingText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}, [loading && style]);
|
||||
|
||||
const renderNoResults = useCallback(() => {
|
||||
if (!showNoResults) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.noResultContainer}>
|
||||
<FormattedText
|
||||
id='mobile.custom_list.no_results'
|
||||
defaultMessage='No Results'
|
||||
style={style.noResultText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}, [showNoResults && style]);
|
||||
|
||||
const renderSectionHeader = useCallback(({section}: {section: SectionListData<UserProfile>}) => {
|
||||
return (
|
||||
<View style={style.sectionWrapper}>
|
||||
<View style={style.sectionContainer}>
|
||||
<Text style={style.sectionText}>{section.id}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}, [style]);
|
||||
|
||||
const renderFlatList = (data: UserProfile[]) => {
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={style.container}
|
||||
data={data}
|
||||
extraData={selectedIds}
|
||||
keyboardShouldPersistTaps='always'
|
||||
{...keyboardDismissProp}
|
||||
keyExtractor={keyExtractor}
|
||||
initialNumToRender={INITIAL_BATCH_TO_RENDER}
|
||||
ListEmptyComponent={renderNoResults}
|
||||
ListFooterComponent={renderLoading}
|
||||
maxToRenderPerBatch={INITIAL_BATCH_TO_RENDER + 1}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={renderItem}
|
||||
scrollEventThrottle={SCROLL_EVENT_THROTTLE}
|
||||
style={style.list}
|
||||
testID={testID}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSectionList = (data: Array<SectionListData<UserProfile>>) => {
|
||||
return (
|
||||
<SectionList
|
||||
contentContainerStyle={style.container}
|
||||
extraData={loading ? false : selectedIds}
|
||||
keyboardShouldPersistTaps='always'
|
||||
{...keyboardDismissProp}
|
||||
keyExtractor={keyExtractor}
|
||||
initialNumToRender={INITIAL_BATCH_TO_RENDER}
|
||||
ListEmptyComponent={renderNoResults}
|
||||
ListFooterComponent={renderLoading}
|
||||
maxToRenderPerBatch={INITIAL_BATCH_TO_RENDER + 1}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={renderItem}
|
||||
renderSectionHeader={renderSectionHeader}
|
||||
scrollEventThrottle={SCROLL_EVENT_THROTTLE}
|
||||
sections={data}
|
||||
style={style.list}
|
||||
stickySectionHeadersEnabled={false}
|
||||
testID={testID}
|
||||
onEndReached={fetchMore}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (isSearch) {
|
||||
return profiles;
|
||||
}
|
||||
return createProfilesSections(profiles);
|
||||
}, [isSearch, profiles]);
|
||||
|
||||
if (isSearch) {
|
||||
return renderFlatList(data as UserProfile[]);
|
||||
}
|
||||
return renderSectionList(data as Array<SectionListData<UserProfile>>);
|
||||
}
|
||||
|
||||
@@ -110,6 +110,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
||||
case Screens.SSO:
|
||||
screen = withIntl(require('@screens/sso').default);
|
||||
break;
|
||||
case Screens.CREATE_DIRECT_MESSAGE:
|
||||
screen = withServerDatabase((require('@screens/create_direct_message').default));
|
||||
break;
|
||||
case Screens.THREAD:
|
||||
screen = withServerDatabase(require('@screens/thread').default);
|
||||
break;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import moment from 'moment-timezone';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import {Permissions, Preferences} from '@constants';
|
||||
import {General, Permissions, Preferences} from '@constants';
|
||||
import {CustomStatusDuration} from '@constants/custom_status';
|
||||
import {UserModel} from '@database/models/server';
|
||||
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
|
||||
@@ -191,3 +191,89 @@ export function confirmOutOfOfficeDisabled(intl: IntlShape, status: string, upda
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
export function isShared(user: UserProfile): boolean {
|
||||
return Boolean(user.remote_id);
|
||||
}
|
||||
|
||||
export function removeUserFromList(userId: string, originalList: UserProfile[]): UserProfile[] {
|
||||
const list = [...originalList];
|
||||
for (let i = list.length - 1; i >= 0; i--) {
|
||||
if (list[i].id === userId) {
|
||||
list.splice(i, 1);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
// Splits the term by a splitStr and composes a list of the parts of
|
||||
// the split concatenated with the rest, forming a set of suggesitons
|
||||
// matchable with startsWith
|
||||
//
|
||||
// E.g.: for "one.two.three" by "." it would yield
|
||||
// ["one.two.three", ".two.three", "two.three", ".three", "three"]
|
||||
export function getSuggestionsSplitBy(term: string, splitStr: string): string[] {
|
||||
const splitTerm = term.split(splitStr);
|
||||
const initialSuggestions = splitTerm.map((st, i) => splitTerm.slice(i).join(splitStr));
|
||||
let suggestions: string[] = [];
|
||||
|
||||
if (splitStr === ' ') {
|
||||
suggestions = initialSuggestions;
|
||||
} else {
|
||||
suggestions = initialSuggestions.reduce((acc, val) => {
|
||||
if (acc.length === 0) {
|
||||
acc.push(val);
|
||||
} else {
|
||||
acc.push(splitStr + val, val);
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
}
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
export function getSuggestionsSplitByMultiple(term: string, splitStrs: string[]): string[] {
|
||||
const suggestions = splitStrs.reduce((acc, val) => {
|
||||
getSuggestionsSplitBy(term, val).forEach((suggestion) => acc.add(suggestion));
|
||||
return acc;
|
||||
}, new Set<string>());
|
||||
|
||||
return [...suggestions];
|
||||
}
|
||||
|
||||
export function filterProfilesMatchingTerm(users: UserProfile[], term: string): UserProfile[] {
|
||||
const lowercasedTerm = term.toLowerCase();
|
||||
let trimmedTerm = lowercasedTerm;
|
||||
if (trimmedTerm.startsWith('@')) {
|
||||
trimmedTerm = trimmedTerm.substring(1);
|
||||
}
|
||||
|
||||
return users.filter((user: UserProfile) => {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const profileSuggestions: string[] = [];
|
||||
const usernameSuggestions = getSuggestionsSplitByMultiple((user.username || '').toLowerCase(), General.AUTOCOMPLETE_SPLIT_CHARACTERS);
|
||||
profileSuggestions.push(...usernameSuggestions);
|
||||
const first = (user.first_name || '').toLowerCase();
|
||||
const last = (user.last_name || '').toLowerCase();
|
||||
const full = first + ' ' + last;
|
||||
profileSuggestions.push(first, last, full);
|
||||
profileSuggestions.push((user.nickname || '').toLowerCase());
|
||||
const email = (user.email || '').toLowerCase();
|
||||
profileSuggestions.push(email);
|
||||
profileSuggestions.push((user.nickname || '').toLowerCase());
|
||||
|
||||
const split = email.split('@');
|
||||
if (split.length > 1) {
|
||||
profileSuggestions.push(split[1]);
|
||||
}
|
||||
|
||||
return profileSuggestions.
|
||||
filter((suggestion) => suggestion !== '').
|
||||
some((suggestion) => suggestion.startsWith(trimmedTerm));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"channel.hasGuests": "Diese Gruppennachricht hat Gäste",
|
||||
"channel.isGuest": "Diese Person ist ein Gast",
|
||||
"channel_header.addMembers": "Mitglieder hinzufügen",
|
||||
"channel_header.directchannel.you": "{displayname} (Sie) ",
|
||||
"channel_header.directchannel.you": "{displayName} (Sie) ",
|
||||
"channel_header.manageMembers": "Mitglieder verwalten",
|
||||
"channel_header.notificationPreference": "Mobile Push-Benachrichtigungen",
|
||||
"channel_header.notificationPreference.all": "Alle",
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
"combined_system_message.removed_from_team.one_you": "You were **removed from the team**.",
|
||||
"combined_system_message.removed_from_team.two": "{firstUser} and {secondUser} were **removed from the team**.",
|
||||
"combined_system_message.you": "You",
|
||||
"create_direct_message.title": "Create Direct Message",
|
||||
"create_post.deactivated": "You are viewing an archived channel with a deactivated user.",
|
||||
"create_post.thread_reply": "Reply to this thread...",
|
||||
"create_post.write": "Write to {channelDisplayName}",
|
||||
@@ -210,6 +211,11 @@
|
||||
"mobile.components.select_server_view.proceed": "Proceed",
|
||||
"mobile.create_channel": "Create",
|
||||
"mobile.create_channel.public": "New Public Channel",
|
||||
"mobile.create_direct_message.add_more": "You can add {remaining, number} more users",
|
||||
"mobile.create_direct_message.cannot_add_more": "You cannot add more users",
|
||||
"mobile.create_direct_message.one_more": "You can add 1 more user",
|
||||
"mobile.create_direct_message.start": "Start",
|
||||
"mobile.create_direct_message.you": "@{username} - you",
|
||||
"mobile.create_post.read_only": "This channel is read-only.",
|
||||
"mobile.custom_list.no_results": "No Results",
|
||||
"mobile.custom_status.choose_emoji": "Choose an emoji",
|
||||
@@ -238,6 +244,7 @@
|
||||
"mobile.join_channel.error": "We couldn't join the channel {displayName}. Please check your connection and try again.",
|
||||
"mobile.link.error.text": "Unable to open the link.",
|
||||
"mobile.link.error.title": "Error",
|
||||
"mobile.loading_members": "Loading Members...",
|
||||
"mobile.login_options.cant_heading": "Can't Log In",
|
||||
"mobile.login_options.enter_credentials": "Enter your login details below.",
|
||||
"mobile.login_options.gitlab": "GitLab",
|
||||
@@ -277,6 +284,7 @@
|
||||
"mobile.oauth.switch_to_browser.error_title": "Sign in error",
|
||||
"mobile.oauth.switch_to_browser.title": "Redirecting...",
|
||||
"mobile.oauth.try_again": "Try again",
|
||||
"mobile.open_gm.error": "",
|
||||
"mobile.permission_denied_dismiss": "Don't Allow",
|
||||
"mobile.permission_denied_retry": "Settings",
|
||||
"mobile.post_info.add_reaction": "Add Reaction",
|
||||
@@ -359,6 +367,7 @@
|
||||
"mobile.unsupported_server.message": "Attachments, link previews, reactions and embed data may not be displayed correctly. If this issue persists contact your System Administrator to upgrade your Mattermost server.",
|
||||
"mobile.unsupported_server.ok": "OK",
|
||||
"mobile.unsupported_server.title": "Unsupported server version",
|
||||
"mobile.user_list.deactivated": "Deactivated",
|
||||
"mobile.write_storage_permission_denied_description": "Save files to your device. Open Settings to grant {applicationName} write access to files on this device.",
|
||||
"mobile.youtube_playback_error.description": "An error occurred while trying to play the YouTube video.\nDetails: {details}",
|
||||
"mobile.youtube_playback_error.title": "YouTube playback error",
|
||||
@@ -429,11 +438,16 @@
|
||||
"status_dropdown.set_offline": "Offline",
|
||||
"status_dropdown.set_online": "Online",
|
||||
"status_dropdown.set_ooo": "Out Of Office",
|
||||
"suggestion.mention.channels": "",
|
||||
"suggestion.mention.morechannels": "",
|
||||
"suggestion.search.direct": "",
|
||||
"suggestion.search.private": "",
|
||||
"suggestion.search.public": "",
|
||||
"team_list.no_other_teams.description": "To join another team, ask a Team Admin for an invitation, or create your own team.",
|
||||
"team_list.no_other_teams.title": "No additional teams to join",
|
||||
"thread.header.thread": "Thread",
|
||||
"thread.header.thread_in": "in {channelName}",
|
||||
"thread.header.thread_dm": "Direct Message Thread",
|
||||
"thread.header.thread_in": "in {channelName}",
|
||||
"thread.noReplies": "No replies yet",
|
||||
"thread.repliesCount": "{repliesCount, number} {repliesCount, plural, one {reply} other {replies}}",
|
||||
"threads.followMessage": "Follow Message",
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"channel.hasGuests": "This group message has guests",
|
||||
"channel.isGuest": "This person is a guest",
|
||||
"channel_header.addMembers": "Add Members",
|
||||
"channel_header.directchannel.you": "{displayname} (you) ",
|
||||
"channel_header.directchannel.you": "{displayName} (you) ",
|
||||
"channel_header.manageMembers": "Manage Members",
|
||||
"channel_header.notificationPreference": "Mobile Notifications",
|
||||
"channel_header.notificationPreference.all": "All",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"channel.hasGuests": "Este grupo tiene huéspedes",
|
||||
"channel.isGuest": "Esta persona es un huésped",
|
||||
"channel_header.addMembers": "Agregar Miembros",
|
||||
"channel_header.directchannel.you": "{displayname} (tu) ",
|
||||
"channel_header.directchannel.you": "{displayName} (tu) ",
|
||||
"channel_header.manageMembers": "Administrar Miembros",
|
||||
"channel_header.notificationPreference": "Notificaciones Móviles",
|
||||
"channel_header.notificationPreference.all": "Todos",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"channel.hasGuests": "Ce groupe dispose d'utilisateurs invités",
|
||||
"channel.isGuest": "Cet utilisateur est un utilisateur invité",
|
||||
"channel_header.addMembers": "Ajouter Membres",
|
||||
"channel_header.directchannel.you": "{displayname} (vous) ",
|
||||
"channel_header.directchannel.you": "{displayName} (vous) ",
|
||||
"channel_header.manageMembers": "Gérer les membres",
|
||||
"channel_header.notificationPreference": "Notifications push mobiles",
|
||||
"channel_header.notificationPreference.all": "Tous",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"channel.hasGuests": "Questo messaggio di gruppo ha degli ospiti",
|
||||
"channel.isGuest": "Questa persona è un ospite",
|
||||
"channel_header.addMembers": "Aggiungi membri",
|
||||
"channel_header.directchannel.you": "{displayname} (tu) ",
|
||||
"channel_header.directchannel.you": "{displayName} (tu) ",
|
||||
"channel_header.manageMembers": "Gestione membri",
|
||||
"channel_header.notificationPreference": "Notifiche push mobile",
|
||||
"channel_header.notificationPreference.all": "Tutti",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"channel.hasGuests": "このグループメッセージにはゲストがいます",
|
||||
"channel.isGuest": "このユーザーはゲストです",
|
||||
"channel_header.addMembers": "メンバーを追加する",
|
||||
"channel_header.directchannel.you": "{displayname} (あなた) ",
|
||||
"channel_header.directchannel.you": "{displayName} (あなた) ",
|
||||
"channel_header.manageMembers": "メンバー管理",
|
||||
"channel_header.notificationPreference": "モバイルプッシュ通知",
|
||||
"channel_header.notificationPreference.all": "すべて",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"channel.hasGuests": "이 그룹 메시지는 게스트가 있습니다.",
|
||||
"channel.isGuest": "이 사람은 게스트입니다.",
|
||||
"channel_header.addMembers": "멤버 추가",
|
||||
"channel_header.directchannel.you": "{displayname} (당신) ",
|
||||
"channel_header.directchannel.you": "{displayName} (당신) ",
|
||||
"channel_header.manageMembers": "회원 관리",
|
||||
"channel_header.notificationPreference": "모바일 푸시 알림",
|
||||
"channel_header.notificationPreference.all": "모두",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"channel.hasGuests": "Dit groepsbericht heeft gasten",
|
||||
"channel.isGuest": "Deze persoon is een gast",
|
||||
"channel_header.addMembers": "Leden toevoegen",
|
||||
"channel_header.directchannel.you": "{displayname} (jij) ",
|
||||
"channel_header.directchannel.you": "{displayName} (jij) ",
|
||||
"channel_header.manageMembers": "Leden beheren",
|
||||
"channel_header.notificationPreference": "Mobiele push notificaties",
|
||||
"channel_header.notificationPreference.all": "Allen",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"channel.hasGuests": "Ten czat grupowy zawiera gości.",
|
||||
"channel.isGuest": "Ta osoba jest gościem",
|
||||
"channel_header.addMembers": "Dodaj Użytkowników",
|
||||
"channel_header.directchannel.you": "{displayname} (Ty) ",
|
||||
"channel_header.directchannel.you": "{displayName} (Ty) ",
|
||||
"channel_header.manageMembers": "Zarządzaj użytkownikami",
|
||||
"channel_header.notificationPreference": "Powiadomienia mobilne push",
|
||||
"channel_header.notificationPreference.all": "Wszystko",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"channel.hasGuests": "Este grupo de mensagem tem convidados",
|
||||
"channel.isGuest": "Esta pessoa é um convidado",
|
||||
"channel_header.addMembers": "Adicionar Membros",
|
||||
"channel_header.directchannel.you": "{displayname} (você) ",
|
||||
"channel_header.directchannel.you": "{displayName} (você) ",
|
||||
"channel_header.manageMembers": "Gerenciar Membros",
|
||||
"channel_header.notificationPreference": "Notificações Móvel",
|
||||
"channel_header.notificationPreference.all": "Todos",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"channel.hasGuests": "Acest mesaj de grup are oaspeți",
|
||||
"channel.isGuest": "Această persoană este invitat",
|
||||
"channel_header.addMembers": "Adăugați membri",
|
||||
"channel_header.directchannel.you": "{displayname} (tine) ",
|
||||
"channel_header.directchannel.you": "{displayName} (tine) ",
|
||||
"channel_header.manageMembers": "Gestioneaza membri",
|
||||
"channel_header.notificationPreference": "Notificări pe mobil",
|
||||
"channel_header.notificationPreference.all": "Toate",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"channel.hasGuests": "В этом групповом сообщении есть гости",
|
||||
"channel.isGuest": "Этот человек гость",
|
||||
"channel_header.addMembers": "Добавить участников",
|
||||
"channel_header.directchannel.you": "{displayname} (это вы) ",
|
||||
"channel_header.directchannel.you": "{displayName} (это вы) ",
|
||||
"channel_header.manageMembers": "Управление участниками",
|
||||
"channel_header.notificationPreference": "Мобильные уведомления",
|
||||
"channel_header.notificationPreference.all": "Все",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"channel.hasGuests": "Bu grup iletisinde konuklar var",
|
||||
"channel.isGuest": "Bu kişi bir konuk",
|
||||
"channel_header.addMembers": "Üye Ekle",
|
||||
"channel_header.directchannel.you": "{displayname} (siz) ",
|
||||
"channel_header.directchannel.you": "{displayName} (siz) ",
|
||||
"channel_header.manageMembers": "Üye Yönetimi",
|
||||
"channel_header.notificationPreference": "Mobil Bildirimler",
|
||||
"channel_header.notificationPreference.all": "Tümü",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"channel.channelHasGuests": "This channel has guests",
|
||||
"channel.hasGuests": "This group message has guests",
|
||||
"channel_header.addMembers": "Додати учасників",
|
||||
"channel_header.directchannel.you": "{displayname} (ви)",
|
||||
"channel_header.directchannel.you": "{displayName} (ви)",
|
||||
"channel_header.manageMembers": "Управління учасниками",
|
||||
"channel_header.notificationPreference": "Надіслати sms повідомлення",
|
||||
"channel_header.notificationPreference.all": "All",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"channel.hasGuests": "此群消息有游客",
|
||||
"channel.isGuest": "该用户是访客",
|
||||
"channel_header.addMembers": "添加成员",
|
||||
"channel_header.directchannel.you": "{displayname} (您) ",
|
||||
"channel_header.directchannel.you": "{displayName} (您) ",
|
||||
"channel_header.manageMembers": "成员管理",
|
||||
"channel_header.notificationPreference": "移动设备推送",
|
||||
"channel_header.notificationPreference.all": "全部",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"channel.hasGuests": "此群組訊息有訪客",
|
||||
"channel.isGuest": "這個使用者是訪客",
|
||||
"channel_header.addMembers": "新增成員",
|
||||
"channel_header.directchannel.you": "{displayname} (你) ",
|
||||
"channel_header.directchannel.you": "{displayName} (你) ",
|
||||
"channel_header.manageMembers": "成員管理",
|
||||
"channel_header.notificationPreference": "行動推播",
|
||||
"channel_header.notificationPreference.all": "全部",
|
||||
|
||||
1
types/api/users.d.ts
vendored
1
types/api/users.d.ts
vendored
@@ -41,6 +41,7 @@ type UserProfile = {
|
||||
timezone?: UserTimezone;
|
||||
is_bot: boolean;
|
||||
last_picture_update: number;
|
||||
remote_id?: string;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user