Merge pull request #7654 from mattermost/gm_to_channel
GM to channel conversion
This commit is contained in:
commit
908e4c08d4
29 changed files with 1218 additions and 50 deletions
|
|
@ -3,12 +3,13 @@
|
|||
|
||||
import {CHANNELS_CATEGORY, DMS_CATEGORY} from '@constants/categories';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {prepareCategoryChannels, queryCategoriesByTeamIds, getCategoryById, prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
|
||||
import {prepareCategoryChannels, queryCategoriesByTeamIds, getCategoryById, prepareCategoriesAndCategoriesChannels, queryCategoryChannelsByChannelId} from '@queries/servers/categories';
|
||||
import {getCurrentUserId} from '@queries/servers/system';
|
||||
import {queryMyTeams} from '@queries/servers/team';
|
||||
import {isDMorGM} from '@utils/channel';
|
||||
import {logError} from '@utils/log';
|
||||
import {logDebug, logError} from '@utils/log';
|
||||
|
||||
import type {Database, Model} from '@nozbe/watermelondb';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
||||
export const deleteCategory = async (serverUrl: string, categoryId: string) => {
|
||||
|
|
@ -91,11 +92,8 @@ export async function addChannelToDefaultCategory(serverUrl: string, channel: Ch
|
|||
categoriesWithChannels.push(cwc);
|
||||
}
|
||||
} else {
|
||||
const categories = await queryCategoriesByTeamIds(database, [teamId]).fetch();
|
||||
const channelCategory = categories.find((c) => c.type === CHANNELS_CATEGORY);
|
||||
if (channelCategory) {
|
||||
const cwc = await channelCategory.toCategoryWithChannels();
|
||||
cwc.channel_ids.unshift(channel.id);
|
||||
const cwc = await prepareAddNonGMDMChannelToDefaultCategory(database, teamId, channel.id);
|
||||
if (cwc) {
|
||||
categoriesWithChannels.push(cwc);
|
||||
}
|
||||
}
|
||||
|
|
@ -108,6 +106,61 @@ export async function addChannelToDefaultCategory(serverUrl: string, channel: Ch
|
|||
|
||||
return {models};
|
||||
} catch (error) {
|
||||
logError('Failed to add channel to default category', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareAddNonGMDMChannelToDefaultCategory(database: Database, teamId: string, channelId: string): Promise<CategoryWithChannels | undefined> {
|
||||
const categories = await queryCategoriesByTeamIds(database, [teamId]).fetch();
|
||||
const channelCategory = categories.find((category) => category.type === CHANNELS_CATEGORY);
|
||||
if (channelCategory) {
|
||||
const cwc = await channelCategory.toCategoryWithChannels();
|
||||
if (cwc.channel_ids.indexOf(channelId) < 0) {
|
||||
cwc.channel_ids.unshift(channelId);
|
||||
return cwc;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function handleConvertedGMCategories(serverUrl: string, channelId: string, targetTeamID: string, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const categoryChannels = await queryCategoryChannelsByChannelId(database, channelId).fetch();
|
||||
|
||||
const categories = await queryCategoriesByTeamIds(database, [targetTeamID]).fetch();
|
||||
const channelCategory = categories.find((category) => category.type === CHANNELS_CATEGORY);
|
||||
|
||||
if (!channelCategory) {
|
||||
logError('Failed to find default category when handling category of converted GM');
|
||||
return {};
|
||||
}
|
||||
|
||||
const models: Model[] = [];
|
||||
|
||||
categoryChannels.forEach((categoryChannel) => {
|
||||
if (categoryChannel.categoryId !== channelCategory.id) {
|
||||
models.push(categoryChannel.prepareDestroyPermanently());
|
||||
}
|
||||
});
|
||||
|
||||
const cwc = await prepareAddNonGMDMChannelToDefaultCategory(database, targetTeamID, channelId);
|
||||
if (cwc) {
|
||||
const model = await prepareCategoryChannels(operator, [cwc]);
|
||||
models.push(...model);
|
||||
} else {
|
||||
logDebug('handleConvertedGMCategories: could not find channel category of target team');
|
||||
}
|
||||
|
||||
if (models.length > 0 && !prepareRecordsOnly) {
|
||||
await operator.batchRecords(models, 'putGMInCorrectCategory');
|
||||
}
|
||||
|
||||
return {models};
|
||||
} catch (error) {
|
||||
logError('Failed to handle category update for GM converted to channel', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
/* eslint-disable max-lines */
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {addChannelToDefaultCategory, storeCategories} from '@actions/local/category';
|
||||
import {addChannelToDefaultCategory, handleConvertedGMCategories, storeCategories} from '@actions/local/category';
|
||||
import {markChannelAsViewed, removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
|
||||
import {switchToGlobalThreads} from '@actions/local/thread';
|
||||
import {loadCallForChannel} from '@calls/actions/calls';
|
||||
|
|
@ -231,6 +231,7 @@ export async function createChannel(serverUrl: string, displayName: string, purp
|
|||
const resolvedModels = await Promise.all(channelModels);
|
||||
models.push(...resolvedModels.flat());
|
||||
}
|
||||
|
||||
const categoriesModels = await addChannelToDefaultCategory(serverUrl, channelData, true);
|
||||
if (categoriesModels.models?.length) {
|
||||
models.push(...categoriesModels.models);
|
||||
|
|
@ -1262,3 +1263,56 @@ export const handleKickFromChannel = async (serverUrl: string, channelId: string
|
|||
logDebug('cannot kick user from channel', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchGroupMessageMembersCommonTeams = async (serverUrl: string, channelId: string) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const teams = await client.getGroupMessageMembersCommonTeams(channelId);
|
||||
return {teams};
|
||||
} catch (error) {
|
||||
logDebug('error on getGroupMessageMembersCommonTeams', getFullErrorMessage(error));
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const convertGroupMessageToPrivateChannel = async (serverUrl: string, channelId: string, targetTeamId: string, displayName: string) => {
|
||||
try {
|
||||
const name = generateChannelNameFromDisplayName(displayName);
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const existingChannel = await getChannelById(database, channelId);
|
||||
if (existingChannel) {
|
||||
EphemeralStore.addConvertingChannel(channelId);
|
||||
}
|
||||
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const updatedChannel = await client.convertGroupMessageToPrivateChannel(channelId, targetTeamId, displayName, name);
|
||||
|
||||
if (existingChannel) {
|
||||
existingChannel.prepareUpdate((channel) => {
|
||||
channel.type = General.PRIVATE_CHANNEL;
|
||||
channel.displayName = displayName;
|
||||
channel.name = name;
|
||||
channel.teamId = targetTeamId;
|
||||
});
|
||||
|
||||
const models: Model[] = [existingChannel];
|
||||
|
||||
const {models: categoryUpdateModels} = await handleConvertedGMCategories(serverUrl, channelId, targetTeamId, true);
|
||||
if (categoryUpdateModels) {
|
||||
models.push(...categoryUpdateModels);
|
||||
}
|
||||
|
||||
await operator.batchRecords(models, 'convertGroupMessageToPrivateChannel');
|
||||
}
|
||||
|
||||
return {updatedChannel};
|
||||
} catch (error) {
|
||||
logError('error on convertGroupMessageToPrivateChannel', getFullErrorMessage(error));
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
} finally {
|
||||
EphemeralStore.removeConvertingChannel(channelId);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {addChannelToDefaultCategory} from '@actions/local/category';
|
||||
import {addChannelToDefaultCategory, handleConvertedGMCategories} from '@actions/local/category';
|
||||
import {
|
||||
markChannelAsViewed, removeCurrentUserFromChannel, setChannelDeleteAt,
|
||||
storeMyChannelsForTeam, updateChannelInfoFromChannel, updateMyChannelFromWebsocket,
|
||||
|
|
@ -12,10 +12,10 @@ import {fetchPostsForChannel} from '@actions/remote/post';
|
|||
import {fetchRolesIfNeeded} from '@actions/remote/role';
|
||||
import {fetchUsersByIds, updateUsersNoLongerVisible} from '@actions/remote/user';
|
||||
import {loadCallForChannel} from '@calls/actions/calls';
|
||||
import {Events} from '@constants';
|
||||
import {Events, General} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {deleteChannelMembership, getChannelById, prepareMyChannelsForTeam, getCurrentChannel} from '@queries/servers/channel';
|
||||
import {getConfig, getCurrentChannelId} from '@queries/servers/system';
|
||||
import {getConfig, getCurrentChannelId, getCurrentTeamId, setCurrentTeamId} from '@queries/servers/system';
|
||||
import {getCurrentUser, getTeammateNameDisplay, getUserById} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
|
|
@ -93,14 +93,35 @@ export async function handleChannelConvertedEvent(serverUrl: string, msg: any) {
|
|||
export async function handleChannelUpdatedEvent(serverUrl: string, msg: any) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const updatedChannel = JSON.parse(msg.data.channel) as Channel;
|
||||
|
||||
if (EphemeralStore.isConvertingChannel(updatedChannel.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const existingChannel = await getChannelById(database, updatedChannel.id);
|
||||
const existingChannelType = existingChannel?.type;
|
||||
|
||||
const updatedChannel = JSON.parse(msg.data.channel);
|
||||
const models: Model[] = await operator.handleChannel({channels: [updatedChannel], prepareRecordsOnly: true});
|
||||
const infoModel = await updateChannelInfoFromChannel(serverUrl, updatedChannel, true);
|
||||
if (infoModel.model) {
|
||||
models.push(...infoModel.model);
|
||||
}
|
||||
operator.batchRecords(models, 'handleChannelUpdatedEvent');
|
||||
|
||||
// This indicates a GM was converted to a private channel
|
||||
if (existingChannelType === General.GM_CHANNEL && updatedChannel.type === General.PRIVATE_CHANNEL) {
|
||||
await handleConvertedGMCategories(serverUrl, updatedChannel.id, updatedChannel.team_id);
|
||||
|
||||
const currentChannelId = await getCurrentChannelId(database);
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
|
||||
// Making sure user is in the correct team
|
||||
if (currentChannelId === updatedChannel.id && currentTeamId !== updatedChannel.team_id) {
|
||||
await setCurrentTeamId(operator, updatedChannel.team_id);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ export interface ClientChannelsMix {
|
|||
searchAllChannels: (term: string, teamIds: string[], archivedOnly?: boolean) => Promise<Channel[]>;
|
||||
updateChannelMemberSchemeRoles: (channelId: string, userId: string, isSchemeUser: boolean, isSchemeAdmin: boolean) => Promise<any>;
|
||||
getMemberInChannel: (channelId: string, userId: string) => Promise<ChannelMembership>;
|
||||
getGroupMessageMembersCommonTeams: (channelId: string) => Promise<Team[]>;
|
||||
convertGroupMessageToPrivateChannel: (channelId: string, teamId: string, displayName: string, name: string) => Promise<Channel>;
|
||||
}
|
||||
|
||||
const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
|
|
@ -350,6 +352,27 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
|
|||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getGroupMessageMembersCommonTeams = (channelId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/common_teams`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
convertGroupMessageToPrivateChannel = (channelId: string, teamId: string, displayName: string, name: string) => {
|
||||
const body = {
|
||||
channel_id: channelId,
|
||||
team_id: teamId,
|
||||
display_name: displayName,
|
||||
name,
|
||||
};
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/convert_to_channel?team-id=${teamId}`,
|
||||
{method: 'post', body},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientChannels;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import React, {useMemo, type ReactNode} from 'react';
|
||||
import {type StyleProp, StyleSheet, Text, type TextStyle, View, type ViewStyle} from 'react-native';
|
||||
import RNButton from 'react-native-button';
|
||||
|
||||
|
|
@ -21,6 +21,7 @@ type Props = ConditionalProps & {
|
|||
testID?: string;
|
||||
onPress: () => void;
|
||||
text: string;
|
||||
iconComponent?: ReactNode;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
|
@ -41,6 +42,7 @@ const Button = ({
|
|||
testID,
|
||||
iconName,
|
||||
iconSize,
|
||||
iconComponent,
|
||||
}: Props) => {
|
||||
const bgStyle = useMemo(() => [
|
||||
buttonBackgroundStyle(theme, size, emphasis, buttonType, buttonState),
|
||||
|
|
@ -61,6 +63,21 @@ const Button = ({
|
|||
[iconSize],
|
||||
);
|
||||
|
||||
let icon: ReactNode;
|
||||
|
||||
if (iconComponent) {
|
||||
icon = iconComponent;
|
||||
} else if (iconName) {
|
||||
icon = (
|
||||
<CompassIcon
|
||||
name={iconName!}
|
||||
size={iconSize}
|
||||
color={StyleSheet.flatten(txtStyle).color}
|
||||
style={styles.icon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RNButton
|
||||
containerStyle={bgStyle}
|
||||
|
|
@ -68,14 +85,7 @@ const Button = ({
|
|||
testID={testID}
|
||||
>
|
||||
<View style={containerStyle}>
|
||||
{Boolean(iconName) &&
|
||||
<CompassIcon
|
||||
name={iconName!}
|
||||
size={iconSize}
|
||||
color={StyleSheet.flatten(txtStyle).color}
|
||||
style={styles.icon}
|
||||
/>
|
||||
}
|
||||
{icon}
|
||||
<Text
|
||||
style={txtStyle}
|
||||
numberOfLines={1}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
// 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 OptionItem from '@components/option_item';
|
||||
import {Screens} from '@constants';
|
||||
import {dismissBottomSheet, goToScreen} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const ConvertToChannelLabel = ({channelId}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const goToConvertToPrivateChannel = useCallback(preventDoubleTap(async () => {
|
||||
await dismissBottomSheet();
|
||||
const title = formatMessage({id: 'channel_info.convert_gm_to_channel.screen_title', defaultMessage: 'Convert to Private Channel'});
|
||||
goToScreen(Screens.CONVERT_GM_TO_CHANNEL, title, {channelId});
|
||||
}), [channelId]);
|
||||
|
||||
return (
|
||||
<OptionItem
|
||||
action={goToConvertToPrivateChannel}
|
||||
icon='lock-outline'
|
||||
label={formatMessage({id: 'channel_info.convert_gm_to_channel', defaultMessage: 'Convert to a Private Channel'})}
|
||||
type='default'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConvertToChannelLabel;
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {ActivityIndicator, type StyleProp, View, type ViewStyle} from 'react-native';
|
||||
import {ActivityIndicator, type StyleProp, View, type ViewStyle, Text, type TextStyle} from 'react-native';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
|
||||
|
|
@ -11,9 +11,18 @@ type LoadingProps = {
|
|||
size?: number | 'small' | 'large';
|
||||
color?: string;
|
||||
themeColor?: keyof Theme;
|
||||
footerText?: string;
|
||||
footerTextStyles?: TextStyle;
|
||||
}
|
||||
|
||||
const Loading = ({containerStyle, size, color, themeColor}: LoadingProps) => {
|
||||
const Loading = ({
|
||||
containerStyle,
|
||||
size,
|
||||
color,
|
||||
themeColor,
|
||||
footerText,
|
||||
footerTextStyles,
|
||||
}: LoadingProps) => {
|
||||
const theme = useTheme();
|
||||
const indicatorColor = themeColor ? theme[themeColor] : color;
|
||||
|
||||
|
|
@ -23,6 +32,10 @@ const Loading = ({containerStyle, size, color, themeColor}: LoadingProps) => {
|
|||
color={indicatorColor}
|
||||
size={size}
|
||||
/>
|
||||
{
|
||||
footerText &&
|
||||
<Text style={footerTextStyles}>{footerText}</Text>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -43,10 +43,14 @@ const hitSlop = {top: 11, bottom: 11, left: 11, right: 11};
|
|||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
actionContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginLeft: 16,
|
||||
},
|
||||
actionSubContainer: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
|
@ -61,8 +65,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
marginTop: DESCRIPTION_MARGIN_TOP,
|
||||
},
|
||||
iconContainer: {marginRight: 16},
|
||||
infoContainer: {marginRight: 2},
|
||||
info: {
|
||||
flex: 1,
|
||||
textAlign: 'right',
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
...typography('Body', 100),
|
||||
},
|
||||
|
|
@ -99,7 +104,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
...typography('Body', 200),
|
||||
},
|
||||
row: {
|
||||
flex: 1,
|
||||
flex: 3,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
};
|
||||
|
|
@ -265,6 +270,7 @@ const OptionItem = ({
|
|||
<Text
|
||||
style={[labelTextStyle, optionLabelTextStyle]}
|
||||
testID={`${testID}.label`}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
|
|
@ -284,16 +290,17 @@ const OptionItem = ({
|
|||
<View style={styles.actionContainer}>
|
||||
{
|
||||
Boolean(info) &&
|
||||
<View style={styles.infoContainer}>
|
||||
<Text
|
||||
style={[styles.info, !actionComponent && styles.iconContainer, destructive && {color: theme.dndIndicator}]}
|
||||
testID={`${testID}.info`}
|
||||
>
|
||||
{info}
|
||||
</Text>
|
||||
</View>
|
||||
<Text
|
||||
style={[styles.info, !actionComponent && styles.iconContainer, destructive && {color: theme.dndIndicator}]}
|
||||
testID={`${testID}.info`}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{info}
|
||||
</Text>
|
||||
}
|
||||
{actionComponent}
|
||||
<View style={styles.actionSubContainer}>
|
||||
{actionComponent}
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export const CHANNEL_FILES = 'ChannelFiles';
|
|||
export const CHANNEL_INFO = 'ChannelInfo';
|
||||
export const CHANNEL_NOTIFICATION_PREFERENCES = 'ChannelNotificationPreferences';
|
||||
export const CODE = 'Code';
|
||||
export const CONVERT_GM_TO_CHANNEL = 'ConvertGMToChannel';
|
||||
export const CREATE_DIRECT_MESSAGE = 'CreateDirectMessage';
|
||||
export const CREATE_OR_EDIT_CHANNEL = 'CreateOrEditChannel';
|
||||
export const CREATE_TEAM = 'CreateTeam';
|
||||
|
|
@ -65,6 +66,7 @@ export const SHARE_FEEDBACK = 'ShareFeedback';
|
|||
export const SNACK_BAR = 'SnackBar';
|
||||
export const SSO = 'SSO';
|
||||
export const TABLE = 'Table';
|
||||
export const TEAM_SELECTOR_LIST = 'TeamSelectorList';
|
||||
export const TERMS_OF_SERVICE = 'TermsOfService';
|
||||
export const THREAD = 'Thread';
|
||||
export const THREAD_FOLLOW_BUTTON = 'ThreadFollowButton';
|
||||
|
|
@ -84,6 +86,7 @@ export default {
|
|||
CHANNEL_INFO,
|
||||
CHANNEL_NOTIFICATION_PREFERENCES,
|
||||
CODE,
|
||||
CONVERT_GM_TO_CHANNEL,
|
||||
CREATE_DIRECT_MESSAGE,
|
||||
CREATE_OR_EDIT_CHANNEL,
|
||||
CREATE_TEAM,
|
||||
|
|
@ -136,6 +139,7 @@ export default {
|
|||
SNACK_BAR,
|
||||
SSO,
|
||||
TABLE,
|
||||
TEAM_SELECTOR_LIST,
|
||||
TERMS_OF_SERVICE,
|
||||
THREAD,
|
||||
THREAD_FOLLOW_BUTTON,
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@ export default {
|
|||
PLUGIN_DISMISSED_POST_ERROR: 'plugin.message_will_be_posted.dismiss_post',
|
||||
SEND_EMAIL_WITH_DEFAULTS_ERROR: 'api.team.invite_members.unable_to_send_email_with_defaults.app_error',
|
||||
TEAM_MEMBERSHIP_DENIAL_ERROR_ID: 'api.team.add_members.user_denied',
|
||||
DUPLICATE_CHANNEL_NAME: 'store.sql_channel.save_channel.exists.app_error',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ function loadTranslation(locale?: string): {[x: string]: string} {
|
|||
require('@formatjs/intl-pluralrules/locale-data/bg');
|
||||
require('@formatjs/intl-numberformat/locale-data/bg');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/bg');
|
||||
require('@formatjs/intl-listformat/locale-data/bg');
|
||||
|
||||
translations = require('@assets/i18n/bg.json');
|
||||
break;
|
||||
|
|
@ -30,6 +31,7 @@ function loadTranslation(locale?: string): {[x: string]: string} {
|
|||
require('@formatjs/intl-pluralrules/locale-data/de');
|
||||
require('@formatjs/intl-numberformat/locale-data/de');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/de');
|
||||
require('@formatjs/intl-listformat/locale-data/de');
|
||||
|
||||
translations = require('@assets/i18n/de.json');
|
||||
break;
|
||||
|
|
@ -37,6 +39,7 @@ function loadTranslation(locale?: string): {[x: string]: string} {
|
|||
require('@formatjs/intl-pluralrules/locale-data/en');
|
||||
require('@formatjs/intl-numberformat/locale-data/en');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/en');
|
||||
require('@formatjs/intl-listformat/locale-data/en');
|
||||
|
||||
translations = require('@assets/i18n/en_AU.json');
|
||||
break;
|
||||
|
|
@ -44,6 +47,7 @@ function loadTranslation(locale?: string): {[x: string]: string} {
|
|||
require('@formatjs/intl-pluralrules/locale-data/es');
|
||||
require('@formatjs/intl-numberformat/locale-data/es');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/es');
|
||||
require('@formatjs/intl-listformat/locale-data/es');
|
||||
|
||||
translations = require('@assets/i18n/es.json');
|
||||
break;
|
||||
|
|
@ -51,6 +55,7 @@ function loadTranslation(locale?: string): {[x: string]: string} {
|
|||
require('@formatjs/intl-pluralrules/locale-data/fa');
|
||||
require('@formatjs/intl-numberformat/locale-data/fa');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/fa');
|
||||
require('@formatjs/intl-listformat/locale-data/fa');
|
||||
|
||||
translations = require('@assets/i18n/fa.json');
|
||||
break;
|
||||
|
|
@ -58,6 +63,7 @@ function loadTranslation(locale?: string): {[x: string]: string} {
|
|||
require('@formatjs/intl-pluralrules/locale-data/fr');
|
||||
require('@formatjs/intl-numberformat/locale-data/fr');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/fr');
|
||||
require('@formatjs/intl-listformat/locale-data/fr');
|
||||
|
||||
translations = require('@assets/i18n/fr.json');
|
||||
break;
|
||||
|
|
@ -65,6 +71,7 @@ function loadTranslation(locale?: string): {[x: string]: string} {
|
|||
require('@formatjs/intl-pluralrules/locale-data/hu');
|
||||
require('@formatjs/intl-numberformat/locale-data/hu');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/hu');
|
||||
require('@formatjs/intl-listformat/locale-data/hu');
|
||||
|
||||
translations = require('@assets/i18n/hu.json');
|
||||
break;
|
||||
|
|
@ -72,6 +79,7 @@ function loadTranslation(locale?: string): {[x: string]: string} {
|
|||
require('@formatjs/intl-pluralrules/locale-data/it');
|
||||
require('@formatjs/intl-numberformat/locale-data/it');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/it');
|
||||
require('@formatjs/intl-listformat/locale-data/it');
|
||||
|
||||
translations = require('@assets/i18n/it.json');
|
||||
break;
|
||||
|
|
@ -79,6 +87,7 @@ function loadTranslation(locale?: string): {[x: string]: string} {
|
|||
require('@formatjs/intl-pluralrules/locale-data/ja');
|
||||
require('@formatjs/intl-numberformat/locale-data/ja');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/ja');
|
||||
require('@formatjs/intl-listformat/locale-data/ja');
|
||||
|
||||
translations = require('@assets/i18n/ja.json');
|
||||
break;
|
||||
|
|
@ -86,6 +95,7 @@ function loadTranslation(locale?: string): {[x: string]: string} {
|
|||
require('@formatjs/intl-pluralrules/locale-data/ko');
|
||||
require('@formatjs/intl-numberformat/locale-data/ko');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/ko');
|
||||
require('@formatjs/intl-listformat/locale-data/ko');
|
||||
|
||||
translations = require('@assets/i18n/ko.json');
|
||||
break;
|
||||
|
|
@ -93,6 +103,7 @@ function loadTranslation(locale?: string): {[x: string]: string} {
|
|||
require('@formatjs/intl-pluralrules/locale-data/nl');
|
||||
require('@formatjs/intl-numberformat/locale-data/nl');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/nl');
|
||||
require('@formatjs/intl-listformat/locale-data/nl');
|
||||
|
||||
translations = require('@assets/i18n/nl.json');
|
||||
break;
|
||||
|
|
@ -100,6 +111,7 @@ function loadTranslation(locale?: string): {[x: string]: string} {
|
|||
require('@formatjs/intl-pluralrules/locale-data/pl');
|
||||
require('@formatjs/intl-numberformat/locale-data/pl');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/pl');
|
||||
require('@formatjs/intl-listformat/locale-data/pl');
|
||||
|
||||
translations = require('@assets/i18n/pl.json');
|
||||
break;
|
||||
|
|
@ -107,6 +119,7 @@ function loadTranslation(locale?: string): {[x: string]: string} {
|
|||
require('@formatjs/intl-pluralrules/locale-data/pt');
|
||||
require('@formatjs/intl-numberformat/locale-data/pt');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/pt');
|
||||
require('@formatjs/intl-listformat/locale-data/pt');
|
||||
|
||||
translations = require('@assets/i18n/pt-BR.json');
|
||||
break;
|
||||
|
|
@ -114,6 +127,7 @@ function loadTranslation(locale?: string): {[x: string]: string} {
|
|||
require('@formatjs/intl-pluralrules/locale-data/ro');
|
||||
require('@formatjs/intl-numberformat/locale-data/ro');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/ro');
|
||||
require('@formatjs/intl-listformat/locale-data/ro');
|
||||
|
||||
translations = require('@assets/i18n/ro.json');
|
||||
break;
|
||||
|
|
@ -121,6 +135,7 @@ function loadTranslation(locale?: string): {[x: string]: string} {
|
|||
require('@formatjs/intl-pluralrules/locale-data/ru');
|
||||
require('@formatjs/intl-numberformat/locale-data/ru');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/ru');
|
||||
require('@formatjs/intl-listformat/locale-data/ru');
|
||||
|
||||
translations = require('@assets/i18n/ru.json');
|
||||
break;
|
||||
|
|
@ -128,6 +143,7 @@ function loadTranslation(locale?: string): {[x: string]: string} {
|
|||
require('@formatjs/intl-pluralrules/locale-data/sv');
|
||||
require('@formatjs/intl-numberformat/locale-data/sv');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/sv');
|
||||
require('@formatjs/intl-listformat/locale-data/sv');
|
||||
|
||||
translations = require('@assets/i18n/sv.json');
|
||||
break;
|
||||
|
|
@ -135,6 +151,7 @@ function loadTranslation(locale?: string): {[x: string]: string} {
|
|||
require('@formatjs/intl-pluralrules/locale-data/tr');
|
||||
require('@formatjs/intl-numberformat/locale-data/tr');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/tr');
|
||||
require('@formatjs/intl-listformat/locale-data/tr');
|
||||
|
||||
translations = require('@assets/i18n/tr.json');
|
||||
break;
|
||||
|
|
@ -142,6 +159,7 @@ function loadTranslation(locale?: string): {[x: string]: string} {
|
|||
require('@formatjs/intl-pluralrules/locale-data/uk');
|
||||
require('@formatjs/intl-numberformat/locale-data/uk');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/uk');
|
||||
require('@formatjs/intl-listformat/locale-data/uk');
|
||||
|
||||
translations = require('@assets/i18n/uk.json');
|
||||
break;
|
||||
|
|
@ -149,6 +167,7 @@ function loadTranslation(locale?: string): {[x: string]: string} {
|
|||
require('@formatjs/intl-pluralrules/locale-data/vi');
|
||||
require('@formatjs/intl-numberformat/locale-data/vi');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/vi');
|
||||
require('@formatjs/intl-listformat/locale-data/vi');
|
||||
|
||||
translations = require('@assets/i18n/uk.json');
|
||||
break;
|
||||
|
|
@ -164,6 +183,7 @@ function loadTranslation(locale?: string): {[x: string]: string} {
|
|||
require('@formatjs/intl-pluralrules/locale-data/en');
|
||||
require('@formatjs/intl-numberformat/locale-data/en');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/en');
|
||||
require('@formatjs/intl-listformat/locale-data/en');
|
||||
|
||||
translations = en;
|
||||
break;
|
||||
|
|
@ -180,6 +200,7 @@ function loadChinesePolyfills() {
|
|||
require('@formatjs/intl-pluralrules/locale-data/zh');
|
||||
require('@formatjs/intl-numberformat/locale-data/zh');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/zh');
|
||||
require('@formatjs/intl-listformat/locale-data/zh');
|
||||
}
|
||||
|
||||
export function getLocaleFromLanguage(lang: string) {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ export const queryCategoriesByTeamIds = (database: Database, teamIds: string[])
|
|||
return database.get<CategoryModel>(CATEGORY).query(Q.where('team_id', Q.oneOf(teamIds)));
|
||||
};
|
||||
|
||||
export const queryCategoryChannelsByChannelId = (database: Database, channelId: string) => {
|
||||
return database.get<CategoryChannelModel>(CATEGORY_CHANNEL).query(Q.where('channel_id', Q.eq(channelId)));
|
||||
};
|
||||
|
||||
export async function prepareCategoriesAndCategoriesChannels(operator: ServerDataOperator, categories: CategoryWithChannels[], prune = false) {
|
||||
try {
|
||||
const {database} = operator;
|
||||
|
|
@ -45,20 +49,34 @@ export async function prepareCategoriesAndCategoriesChannels(operator: ServerDat
|
|||
const models = await Promise.all(modelPromises);
|
||||
const flattenedModels = models.flat();
|
||||
|
||||
const teamIdToChannelIds = new Map<String, Set<String>>();
|
||||
categories.forEach((category) => {
|
||||
const value = teamIdToChannelIds.get(category.team_id) || new Set();
|
||||
category.channel_ids.forEach(value.add, value);
|
||||
teamIdToChannelIds.set(category.team_id, value);
|
||||
});
|
||||
|
||||
if (prune && categories.length) {
|
||||
const remoteCategoryIds = new Set(categories.map((cat) => cat.id));
|
||||
|
||||
// If the passed categories have more than one team, we want to update across teams
|
||||
const teamIds = pluckUnique('team_id')(categories) as string[];
|
||||
const localCategories = await queryCategoriesByTeamIds(database, teamIds).fetch();
|
||||
const customCategories = localCategories.filter((c) => c.type === 'custom');
|
||||
for await (const custom of customCategories) {
|
||||
if (!remoteCategoryIds.has(custom.id)) {
|
||||
const categoryChannels = await custom.categoryChannels.fetch();
|
||||
for (const cc of categoryChannels) {
|
||||
|
||||
for await (const localCategory of localCategories) {
|
||||
const localCategoryChannels = await localCategory.categoryChannels.fetch();
|
||||
|
||||
if (remoteCategoryIds.has(localCategory.id)) {
|
||||
for (const localCC of localCategoryChannels) {
|
||||
if (!teamIdToChannelIds.get(localCategory.teamId)?.has(localCC.channelId)) {
|
||||
flattenedModels.push(localCC.prepareDestroyPermanently());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const cc of localCategoryChannels) {
|
||||
flattenedModels.push(cc.prepareDestroyPermanently());
|
||||
}
|
||||
flattenedModels.push(custom.prepareDestroyPermanently());
|
||||
flattenedModels.push(localCategory.prepareDestroyPermanently());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {Preferences} from '@constants';
|
|||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {PUSH_PROXY_STATUS_UNKNOWN} from '@constants/push_proxy';
|
||||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type ConfigModel from '@typings/database/models/servers/config';
|
||||
|
|
@ -434,6 +435,22 @@ export async function setCurrentChannelId(operator: ServerDataOperator, channelI
|
|||
}
|
||||
}
|
||||
|
||||
export async function setCurrentTeamId(operator: ServerDataOperator, teamId: string) {
|
||||
try {
|
||||
const models = await prepareCommonSystemValues(operator, {
|
||||
currentTeamId: teamId,
|
||||
});
|
||||
if (models) {
|
||||
await operator.batchRecords(models, 'setCurrentTeamId');
|
||||
}
|
||||
|
||||
return {currentTeamId: teamId};
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function setCurrentTeamAndChannelId(operator: ServerDataOperator, teamId?: string, channelId?: string) {
|
||||
try {
|
||||
const models = await prepareCommonSystemValues(operator, {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import {type Edge, SafeAreaView} from 'react-native-safe-area-context';
|
|||
|
||||
import ChannelInfoEnableCalls from '@calls/components/channel_info_enable_calls';
|
||||
import ChannelActions from '@components/channel_actions';
|
||||
import ConvertToChannelLabel from '@components/channel_actions/convert_to_channel/convert_to_channel_label';
|
||||
import {General} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
|
|
@ -32,6 +34,8 @@ type Props = {
|
|||
canManageMembers: boolean;
|
||||
isCRTEnabled: boolean;
|
||||
canManageSettings: boolean;
|
||||
isGuestUser: boolean;
|
||||
isConvertGMFeatureAvailable: boolean;
|
||||
}
|
||||
|
||||
const edges: Edge[] = ['bottom', 'left', 'right'];
|
||||
|
|
@ -61,6 +65,8 @@ const ChannelInfo = ({
|
|||
isCallsEnabledInChannel,
|
||||
canManageMembers,
|
||||
canManageSettings,
|
||||
isGuestUser,
|
||||
isConvertGMFeatureAvailable,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
|
|
@ -77,6 +83,8 @@ const ChannelInfo = ({
|
|||
useNavButtonPressed(closeButtonId, componentId, onPressed, [onPressed]);
|
||||
useAndroidHardwareBackHandler(componentId, onPressed);
|
||||
|
||||
const convertGMOptionAvailable = isConvertGMFeatureAvailable && type === General.GM_CHANNEL && !isGuestUser;
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
edges={edges}
|
||||
|
|
@ -111,6 +119,12 @@ const ChannelInfo = ({
|
|||
canManageSettings={canManageSettings}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
{convertGMOptionAvailable &&
|
||||
<>
|
||||
<ConvertToChannelLabel channelId={channelId}/>
|
||||
<View style={styles.separator}/>
|
||||
</>
|
||||
}
|
||||
{canEnableDisableCalls &&
|
||||
<>
|
||||
<ChannelInfoEnableCalls
|
||||
|
|
|
|||
|
|
@ -113,6 +113,15 @@ const enhanced = withObservables([], ({serverUrl, database}: Props) => {
|
|||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
const isGuestUser = currentUser.pipe(
|
||||
switchMap((u) => (u ? of$(u.isGuest) : of$(false))),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
const isConvertGMFeatureAvailable = observeConfigValue(database, 'Version').pipe(
|
||||
switchMap((version) => of$(isMinimumServerVersion(version || '', 9, 1))),
|
||||
);
|
||||
|
||||
return {
|
||||
type,
|
||||
canEnableDisableCalls,
|
||||
|
|
@ -120,6 +129,8 @@ const enhanced = withObservables([], ({serverUrl, database}: Props) => {
|
|||
canManageMembers,
|
||||
isCRTEnabled: observeIsCRTEnabled(database),
|
||||
canManageSettings,
|
||||
isGuestUser,
|
||||
isConvertGMFeatureAvailable,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
44
app/screens/convert_gm_to_channel/channel_name_input.tsx
Normal file
44
app/screens/convert_gm_to_channel/channel_name_input.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// 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 FloatingTextInput from '@components/floating_text_input_label';
|
||||
import {Channel} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {getKeyboardAppearanceFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
error?: string;
|
||||
onChange: (text: string) => void;
|
||||
}
|
||||
|
||||
export const ChannelNameInput = ({error, onChange}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const theme = useTheme();
|
||||
|
||||
const labelDisplayName = formatMessage({id: 'channel_modal.name', defaultMessage: 'Name'});
|
||||
const placeholder = formatMessage({id: 'channel_modal.name', defaultMessage: 'Channel Name'});
|
||||
|
||||
return (
|
||||
<FloatingTextInput
|
||||
autoCorrect={false}
|
||||
autoCapitalize='none'
|
||||
blurOnSubmit={false}
|
||||
disableFullscreenUI={true}
|
||||
enablesReturnKeyAutomatically={true}
|
||||
label={labelDisplayName}
|
||||
placeholder={placeholder}
|
||||
maxLength={Channel.MAX_CHANNEL_NAME_LENGTH}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
returnKeyType='next'
|
||||
showErrorIcon={true}
|
||||
spellCheck={false}
|
||||
testID='gonvert_gm_to_channel.channel_display_name.input'
|
||||
theme={theme}
|
||||
error={error}
|
||||
onChangeText={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
155
app/screens/convert_gm_to_channel/convert_gm_to_channel.tsx
Normal file
155
app/screens/convert_gm_to_channel/convert_gm_to_channel.tsx
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import {fetchChannelMemberships, fetchGroupMessageMembersCommonTeams} from '@actions/remote/channel';
|
||||
import {PER_PAGE_DEFAULT} from '@client/rest/constants';
|
||||
import Loading from '@components/loading';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import ConvertGMToChannelForm from './convert_gm_to_channel_form';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
currentUserId?: string;
|
||||
}
|
||||
|
||||
const loadingIndicatorTimeout = 1200;
|
||||
|
||||
const matchUserProfiles = (users: UserProfile[], members: ChannelMembership[], currentUserId: string) => {
|
||||
// Gotta make sure we use profiles that are in members.
|
||||
// See comment in fetchChannelMemberships for more details.
|
||||
|
||||
const usersById: {[id: string]: UserProfile} = {};
|
||||
users.forEach((profile) => {
|
||||
if (profile.id !== currentUserId) {
|
||||
usersById[profile.id] = profile;
|
||||
}
|
||||
});
|
||||
|
||||
const filteredUsers: UserProfile[] = [];
|
||||
members.forEach((member) => {
|
||||
if (usersById[member.user_id]) {
|
||||
filteredUsers.push(usersById[member.user_id]);
|
||||
}
|
||||
});
|
||||
|
||||
return filteredUsers;
|
||||
};
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
loadingContainer: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
gap: 24,
|
||||
},
|
||||
text: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
...typography('Body', 300, 'SemiBold'),
|
||||
},
|
||||
container: {
|
||||
paddingVertical: 24,
|
||||
paddingHorizontal: 20,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 24,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const ConvertGMToChannel = ({
|
||||
channelId,
|
||||
currentUserId,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleFromTheme(theme);
|
||||
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const [loadingAnimationTimeout, setLoadingAnimationTimeout] = useState(false);
|
||||
const [commonTeamsFetched, setCommonTeamsFetched] = useState(false);
|
||||
const [channelMembersFetched, setChannelMembersFetched] = useState(false);
|
||||
const [commonTeams, setCommonTeams] = useState<Team[]>([]);
|
||||
const [profiles, setProfiles] = useState<UserProfile[]>([]);
|
||||
|
||||
const serverUrl = useServerUrl();
|
||||
const mounted = useRef(false);
|
||||
|
||||
const loadingAnimationTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
useEffect(() => {
|
||||
loadingAnimationTimeoutRef.current = setTimeout(() => setLoadingAnimationTimeout(true), loadingIndicatorTimeout);
|
||||
async function work() {
|
||||
const {teams} = await fetchGroupMessageMembersCommonTeams(serverUrl, channelId);
|
||||
if (!teams || !mounted.current) {
|
||||
return;
|
||||
}
|
||||
setCommonTeams(teams);
|
||||
setCommonTeamsFetched(true);
|
||||
}
|
||||
|
||||
work();
|
||||
|
||||
return () => {
|
||||
clearTimeout(loadingAnimationTimeoutRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options: GetUsersOptions = {sort: 'admin', active: true, per_page: PER_PAGE_DEFAULT};
|
||||
fetchChannelMemberships(serverUrl, channelId, options, true).then(({users, members}) => {
|
||||
if (!mounted.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (users.length) {
|
||||
setProfiles(matchUserProfiles(users, members, currentUserId));
|
||||
}
|
||||
|
||||
setChannelMembersFetched(true);
|
||||
});
|
||||
}, [serverUrl, channelId, currentUserId]);
|
||||
|
||||
const showLoader = !loadingAnimationTimeout || !commonTeamsFetched || !channelMembersFetched;
|
||||
|
||||
if (showLoader) {
|
||||
return (
|
||||
<Loading
|
||||
containerStyle={styles.loadingContainer}
|
||||
size='large'
|
||||
color={theme.buttonBg}
|
||||
footerText={formatMessage({id: 'channel_info.convert_gm_to_channel.loading.footer', defaultMessage: 'Fetching details...'})}
|
||||
footerTextStyles={styles.text}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ConvertGMToChannelForm
|
||||
commonTeams={commonTeams}
|
||||
profiles={profiles}
|
||||
channelId={channelId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConvertGMToChannel;
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import {convertGroupMessageToPrivateChannel, switchToChannelById} from '@actions/remote/channel';
|
||||
import Button from '@components/button';
|
||||
import Loading from '@components/loading';
|
||||
import {ServerErrors} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {isErrorWithMessage, isServerError} from '@utils/errors';
|
||||
import {logError} from '@utils/log';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
import {ChannelNameInput} from '../channel_name_input';
|
||||
import MessageBox from '../message_box/message_box';
|
||||
import {TeamSelector} from '../team_selector';
|
||||
|
||||
import {NoCommonTeamForm} from './no_common_teams_form';
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
paddingVertical: 24,
|
||||
paddingHorizontal: 20,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 24,
|
||||
},
|
||||
errorMessage: {
|
||||
color: theme.dndIndicator,
|
||||
},
|
||||
loadingContainerStyle: {
|
||||
marginRight: 10,
|
||||
padding: 0,
|
||||
top: -2,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
commonTeams: Team[];
|
||||
profiles: UserProfile[];
|
||||
locale?: string;
|
||||
teammateNameDisplay?: string;
|
||||
}
|
||||
|
||||
export const ConvertGMToChannelForm = ({
|
||||
channelId,
|
||||
commonTeams,
|
||||
profiles,
|
||||
locale,
|
||||
teammateNameDisplay,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleFromTheme(theme);
|
||||
const serverUrl = useServerUrl();
|
||||
const {formatList, formatMessage} = useIntl();
|
||||
|
||||
const [selectedTeam, setSelectedTeam] = useState<Team>(commonTeams[0]);
|
||||
const [newChannelName, setNewChannelName] = useState<string>('');
|
||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||
const [channelNameErrorMessage, setChannelNameErrorMessage] = useState<string>('');
|
||||
const [conversionInProgress, setConversionInProgress] = useState(false);
|
||||
|
||||
const userDisplayNames = useMemo(() => profiles.map((profile) => displayUsername(profile, locale, teammateNameDisplay)), [profiles, teammateNameDisplay, locale]);
|
||||
const submitButtonEnabled = !conversionInProgress && selectedTeam && newChannelName.trim();
|
||||
|
||||
const handleOnPress = useCallback(preventDoubleTap(async () => {
|
||||
if (!submitButtonEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setConversionInProgress(true);
|
||||
|
||||
const {updatedChannel, error} = await convertGroupMessageToPrivateChannel(serverUrl, channelId, selectedTeam.id, newChannelName);
|
||||
if (error) {
|
||||
if (isServerError(error) && error.server_error_id === ServerErrors.DUPLICATE_CHANNEL_NAME && isErrorWithMessage(error)) {
|
||||
setChannelNameErrorMessage(error.message);
|
||||
} else if (isErrorWithMessage(error)) {
|
||||
setErrorMessage(error.message);
|
||||
} else {
|
||||
setErrorMessage(formatMessage({id: 'channel_info.convert_gm_to_channel.conversion_error', defaultMessage: 'Something went wrong. Failed to convert Group Message to Private Channel.'}));
|
||||
}
|
||||
|
||||
setConversionInProgress(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!updatedChannel) {
|
||||
logError('No updated channel received from server when converting GM to private channel');
|
||||
setErrorMessage(formatMessage({id: 'channel_info.convert_gm_to_channel.conversion_error', defaultMessage: 'Something went wrong. Failed to convert Group Message to Private Channel.'}));
|
||||
setConversionInProgress(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorMessage('');
|
||||
switchToChannelById(serverUrl, updatedChannel.id, selectedTeam.id);
|
||||
setConversionInProgress(false);
|
||||
}), [selectedTeam, newChannelName, submitButtonEnabled]);
|
||||
|
||||
if (commonTeams.length === 0) {
|
||||
return (
|
||||
<NoCommonTeamForm containerStyles={styles.container}/>
|
||||
);
|
||||
}
|
||||
|
||||
const messageBoxHeader = formatMessage({
|
||||
id: 'channel_info.convert_gm_to_channel.warning.header',
|
||||
defaultMessage: 'Conversation history will be visible to any channel members',
|
||||
});
|
||||
|
||||
const textConvert = formatMessage({
|
||||
id: 'channel_info.convert_gm_to_channel.button_text',
|
||||
defaultMessage: 'Convert to Private Channel',
|
||||
});
|
||||
|
||||
const textConverting = formatMessage({
|
||||
id: 'channel_info.convert_gm_to_channel.button_text_converting',
|
||||
defaultMessage: 'Converting...',
|
||||
});
|
||||
|
||||
const confirmButtonText = conversionInProgress ? textConverting : textConvert;
|
||||
const defaultUserDisplayNames = formatMessage({id: 'channel_info.convert_gm_to_channel.warning.body.yourself', defaultMessage: 'yourself'});
|
||||
const memberNames = profiles.length > 0 ? formatList(userDisplayNames) : defaultUserDisplayNames;
|
||||
const messageBoxBody = formatMessage({
|
||||
id: 'channel_info.convert_gm_to_channel.warning.bodyXXXX',
|
||||
defaultMessage: 'You are about to convert the Group Message with {memberNames} to a Channel. This cannot be undone.',
|
||||
}, {
|
||||
memberNames,
|
||||
});
|
||||
|
||||
const buttonIcon = conversionInProgress ? (
|
||||
<Loading
|
||||
containerStyle={styles.loadingContainerStyle}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.32)}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<MessageBox
|
||||
header={messageBoxHeader}
|
||||
body={messageBoxBody}
|
||||
/>
|
||||
{
|
||||
commonTeams.length > 1 &&
|
||||
<TeamSelector
|
||||
commonTeams={commonTeams}
|
||||
onSelectTeam={setSelectedTeam}
|
||||
selectedTeamId={selectedTeam?.id}
|
||||
/>
|
||||
}
|
||||
<ChannelNameInput
|
||||
onChange={setNewChannelName}
|
||||
error={channelNameErrorMessage}
|
||||
/>
|
||||
<Button
|
||||
onPress={handleOnPress}
|
||||
text={confirmButtonText}
|
||||
theme={theme}
|
||||
buttonType={submitButtonEnabled ? 'destructive' : 'disabled'}
|
||||
size='lg'
|
||||
iconComponent={buttonIcon}
|
||||
/>
|
||||
{
|
||||
errorMessage &&
|
||||
<Text style={styles.errorMessage}>
|
||||
{errorMessage}
|
||||
</Text>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import {switchMap, distinctUntilChanged} from '@nozbe/watermelondb/utils/rx';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
|
||||
import {observeCurrentUser, observeTeammateNameDisplay} from '@queries/servers/user';
|
||||
|
||||
import {ConvertGMToChannelForm} from './convert_gm_to_channel_form';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const locale = observeCurrentUser(database).pipe(
|
||||
switchMap((user) => of$(user?.locale)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
const teammateNameDisplay = observeTeammateNameDisplay(database);
|
||||
|
||||
return {
|
||||
locale,
|
||||
teammateNameDisplay,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(ConvertGMToChannelForm));
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// 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 {View, type ViewStyle} from 'react-native';
|
||||
|
||||
import Button from '@components/button';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {popTopScreen} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
import MessageBox from '../message_box/message_box';
|
||||
|
||||
type Props = {
|
||||
containerStyles: ViewStyle;
|
||||
}
|
||||
|
||||
const handleOnPress = preventDoubleTap(() => {
|
||||
popTopScreen();
|
||||
});
|
||||
|
||||
export const NoCommonTeamForm = ({
|
||||
containerStyles,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const header = formatMessage({
|
||||
id: 'channel_info.convert_gm_to_channel.warning.no_teams.header',
|
||||
defaultMessage: 'Unable to convert to a channel because group members are part of different teams',
|
||||
});
|
||||
|
||||
const body = formatMessage({
|
||||
id: 'channel_info.convert_gm_to_channel.warning.no_teams.body',
|
||||
defaultMessage: 'Group Message cannot be converted to a channel because members are not a part of the same team. Add all members to a single team to convert this group message to a channel.',
|
||||
});
|
||||
|
||||
const buttonText = formatMessage({
|
||||
id: 'generic.back',
|
||||
defaultMessage: 'Back',
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={containerStyles}>
|
||||
<MessageBox
|
||||
header={header}
|
||||
body={body}
|
||||
type='danger'
|
||||
/>
|
||||
<Button
|
||||
onPress={handleOnPress}
|
||||
text={buttonText}
|
||||
theme={theme}
|
||||
size='lg'
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
24
app/screens/convert_gm_to_channel/index.tsx
Normal file
24
app/screens/convert_gm_to_channel/index.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// 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 {observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeTeammateNameDisplay} from '@queries/servers/user';
|
||||
|
||||
import ConvertGMToChannel from './convert_gm_to_channel';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhance = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const currentUserId = observeCurrentUserId(database);
|
||||
const teammateNameDisplay = observeTeammateNameDisplay(database);
|
||||
|
||||
return {
|
||||
currentUserId,
|
||||
teammateNameDisplay,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhance(ConvertGMToChannel));
|
||||
136
app/screens/convert_gm_to_channel/message_box/message_box.tsx
Normal file
136
app/screens/convert_gm_to_channel/message_box/message_box.tsx
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
// 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 CompassIcon from '@components/compass_icon';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type MessageBoxTypes = 'default' | 'danger'
|
||||
|
||||
type Props = {
|
||||
header: string;
|
||||
body: string;
|
||||
type?: MessageBoxTypes;
|
||||
}
|
||||
|
||||
const getBaseStyles = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: changeOpacity(theme.sidebarTextActiveBorder, 0.08),
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
borderColor: changeOpacity(theme.sidebarTextActiveBorder, 0.16),
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
padding: 16,
|
||||
gap: 12,
|
||||
},
|
||||
icon: {
|
||||
marginTop: 5,
|
||||
fontSize: 20,
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderWidth: 3,
|
||||
color: theme.sidebarTextActiveBorder,
|
||||
borderColor: theme.sidebarTextActiveBorder,
|
||||
borderRadius: 14,
|
||||
textAlign: 'center',
|
||||
},
|
||||
iconContainer: {},
|
||||
textContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
flex: 1,
|
||||
},
|
||||
heading: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
},
|
||||
body: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 100),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const getDefaultStylesFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: changeOpacity(theme.sidebarTextActiveBorder, 0.08),
|
||||
borderColor: changeOpacity(theme.sidebarTextActiveBorder, 0.16),
|
||||
},
|
||||
icon: {
|
||||
color: theme.sidebarTextActiveBorder,
|
||||
borderColor: theme.sidebarTextActiveBorder,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const getDangerStylesFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: changeOpacity(theme.dndIndicator, 0.08),
|
||||
borderColor: changeOpacity(theme.dndIndicator, 0.16),
|
||||
},
|
||||
icon: {
|
||||
color: theme.dndIndicator,
|
||||
borderColor: theme.dndIndicator,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const getStyleFromTheme = (theme: Theme, kind: MessageBoxTypes | undefined) => {
|
||||
let kindStyles;
|
||||
switch (kind) {
|
||||
case 'danger': {
|
||||
kindStyles = getDangerStylesFromTheme(theme);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
kindStyles = getDefaultStylesFromTheme(theme);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return kindStyles;
|
||||
};
|
||||
|
||||
const MessageBox = ({
|
||||
header,
|
||||
body,
|
||||
type,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const baseStyle = getBaseStyles(theme);
|
||||
const kindStyle = getStyleFromTheme(theme, type);
|
||||
|
||||
return (
|
||||
<View style={[baseStyle.container, kindStyle.container]}>
|
||||
<View style={baseStyle.iconContainer}>
|
||||
<CompassIcon
|
||||
name='exclamation-thick'
|
||||
style={[baseStyle.icon, kindStyle.icon]}
|
||||
/>
|
||||
</View>
|
||||
<View style={baseStyle.textContainer}>
|
||||
<View>
|
||||
<Text style={baseStyle.heading}>
|
||||
{header}
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text style={baseStyle.body}>
|
||||
{body}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageBox;
|
||||
67
app/screens/convert_gm_to_channel/team_selector.tsx
Normal file
67
app/screens/convert_gm_to_channel/team_selector.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// 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 {Platform} from 'react-native';
|
||||
|
||||
import OptionItem from '@components/option_item';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {dismissBottomSheet, goToScreen} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
teamSelector: {
|
||||
borderTopWidth: 1,
|
||||
borderBottomWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
},
|
||||
labelContainerStyle: {
|
||||
flexShrink: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type Props = {
|
||||
commonTeams: Team[];
|
||||
onSelectTeam: (team: Team) => void;
|
||||
selectedTeamId?: string;
|
||||
}
|
||||
|
||||
export const TeamSelector = ({commonTeams, onSelectTeam, selectedTeamId}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleFromTheme(theme);
|
||||
|
||||
const label = formatMessage({id: 'channel_into.convert_gm_to_channel.team_selector.label', defaultMessage: 'Team'});
|
||||
const placeholder = formatMessage({id: 'channel_into.convert_gm_to_channel.team_selector.placeholder', defaultMessage: 'Select a Team'});
|
||||
|
||||
const selectedTeam = useMemo(() => commonTeams.find((t) => t.id === selectedTeamId), [commonTeams, selectedTeamId]);
|
||||
|
||||
const selectTeam = useCallback((teamId: string) => {
|
||||
const team = commonTeams.find((t) => t.id === teamId);
|
||||
if (team) {
|
||||
onSelectTeam(team);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const goToTeamSelectorList = useCallback(preventDoubleTap(async () => {
|
||||
await dismissBottomSheet();
|
||||
const title = formatMessage({id: 'channel_info.convert_gm_to_channel.team_selector_list.title', defaultMessage: 'Select Team'});
|
||||
goToScreen(Screens.TEAM_SELECTOR_LIST, title, {teams: commonTeams, selectTeam, selectedTeamId});
|
||||
}), [commonTeams, selectTeam, selectedTeamId]);
|
||||
|
||||
return (
|
||||
<OptionItem
|
||||
action={goToTeamSelectorList}
|
||||
containerStyle={styles.teamSelector}
|
||||
label={label}
|
||||
type={Platform.select({ios: 'arrow', default: 'default'})}
|
||||
info={selectedTeam ? selectedTeam.display_name : placeholder}
|
||||
labelContainerStyle={styles.labelContainerStyle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {debounce} from 'lodash';
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import SearchBar from '@components/search';
|
||||
import TeamList from '@components/team_list';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {popTopScreen} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
padding: 12,
|
||||
},
|
||||
listContainer: {
|
||||
marginTop: 12,
|
||||
},
|
||||
});
|
||||
|
||||
type Props = {
|
||||
teams: Team[];
|
||||
selectTeam: (teamId: string) => void;
|
||||
}
|
||||
|
||||
const TeamSelectorList = ({teams, selectTeam}: Props) => {
|
||||
const theme = useTheme();
|
||||
const [filteredTeams, setFilteredTeam] = useState(teams);
|
||||
const color = useMemo(() => changeOpacity(theme.centerChannelColor, 0.72), [theme]);
|
||||
|
||||
const handleOnChangeSearchText = useCallback(debounce((searchTerm: string) => {
|
||||
if (searchTerm === '') {
|
||||
setFilteredTeam(teams);
|
||||
} else {
|
||||
setFilteredTeam(teams.filter((team) => team.display_name.includes(searchTerm) || team.name.includes(searchTerm)));
|
||||
}
|
||||
}, 200), [teams]);
|
||||
|
||||
const handleOnPress = useCallback(preventDoubleTap((teamId: string) => {
|
||||
selectTeam(teamId);
|
||||
popTopScreen();
|
||||
}), []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<SearchBar
|
||||
autoCapitalize='none'
|
||||
autoFocus={true}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
placeholderTextColor={color}
|
||||
searchIconColor={color}
|
||||
testID='convert_gm_to_channel_team_search_bar'
|
||||
onChangeText={handleOnChangeSearchText}
|
||||
/>
|
||||
<View style={styles.listContainer}>
|
||||
<TeamList
|
||||
teams={filteredTeams}
|
||||
onPress={handleOnPress}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamSelectorList;
|
||||
|
|
@ -90,6 +90,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
case Screens.CODE:
|
||||
screen = withServerDatabase(require('@screens/code').default);
|
||||
break;
|
||||
case Screens.CONVERT_GM_TO_CHANNEL:
|
||||
screen = withServerDatabase(require('@screens/convert_gm_to_channel').default);
|
||||
break;
|
||||
case Screens.CREATE_OR_EDIT_CHANNEL:
|
||||
screen = withServerDatabase(require('@screens/create_or_edit_channel').default);
|
||||
break;
|
||||
|
|
@ -242,6 +245,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
case Screens.TABLE:
|
||||
screen = withServerDatabase(require('@screens/table').default);
|
||||
break;
|
||||
case Screens.TEAM_SELECTOR_LIST:
|
||||
screen = withServerDatabase(require('@screens/convert_gm_to_channel/team_selector_list').default);
|
||||
break;
|
||||
case Screens.TERMS_OF_SERVICE:
|
||||
screen = withServerDatabase(require('@screens/terms_of_service').default);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -121,6 +121,18 @@
|
|||
"channel_info.close_gm": "Close group message",
|
||||
"channel_info.close_gm_channel": "Are you sure you want to close this group message? This will remove it from your home screen, but you can always open it again.",
|
||||
"channel_info.convert_failed": "We were unable to convert {displayName} to a private channel.",
|
||||
"channel_info.convert_gm_to_channel": "Convert to a Private Channel",
|
||||
"channel_info.convert_gm_to_channel.button_text": "Convert to Private Channel",
|
||||
"channel_info.convert_gm_to_channel.button_text_converting": "Converting...",
|
||||
"channel_info.convert_gm_to_channel.conversion_error": "Something went wrong. Failed to convert Group Message to Private Channel.",
|
||||
"channel_info.convert_gm_to_channel.loading.footer": "Fetching details...",
|
||||
"channel_info.convert_gm_to_channel.screen_title": "Convert to Private Channel",
|
||||
"channel_info.convert_gm_to_channel.team_selector_list.title": "Select Team",
|
||||
"channel_info.convert_gm_to_channel.warning.body.yourself": "yourself",
|
||||
"channel_info.convert_gm_to_channel.warning.bodyXXXX": "You are about to convert the Group Message with {memberNames} to a Channel. This cannot be undone.",
|
||||
"channel_info.convert_gm_to_channel.warning.header": "Conversation history will be visible to any channel members",
|
||||
"channel_info.convert_gm_to_channel.warning.no_teams.body": "Group Message cannot be converted to a channel because members are not a part of the same team. Add all members to a single team to convert this group message to a channel.",
|
||||
"channel_info.convert_gm_to_channel.warning.no_teams.header": "Unable to convert to a channel because group members are part of different teams",
|
||||
"channel_info.convert_private": "Convert to private channel",
|
||||
"channel_info.convert_private_description": "When you convert {displayName} to a private channel, history and membership are preserved. Publicly shared files remain accessible to anyone with the link. Membership in a private channel is by invitation only.\n\nThe change is permanent and cannot be undone.\n\nAre you sure you want to convert {displayName} to a private channel?",
|
||||
"channel_info.convert_private_success": "{displayName} is now a private channel.",
|
||||
|
|
@ -160,6 +172,8 @@
|
|||
"channel_info.unarchive_description": "Are you sure you want to unarchive the {term} {name}?",
|
||||
"channel_info.unarchive_failed": "An error occurred trying to unarchive the channel {displayName}",
|
||||
"channel_info.unarchive_title": "Unarchive {term}",
|
||||
"channel_into.convert_gm_to_channel.team_selector.label": "Team",
|
||||
"channel_into.convert_gm_to_channel.team_selector.placeholder": "Select a Team",
|
||||
"channel_intro.createdBy": "Created by {user} on {date}",
|
||||
"channel_intro.createdOn": "Created on {date}",
|
||||
"channel_list.channels_category": "Channels",
|
||||
|
|
@ -322,6 +336,7 @@
|
|||
"general_settings.help": "Help",
|
||||
"general_settings.notifications": "Notifications",
|
||||
"general_settings.report_problem": "Report a problem",
|
||||
"generic.back": "Back",
|
||||
"get_post_link_modal.title": "Copy Link",
|
||||
"global_threads.allThreads": "All Your Threads",
|
||||
"global_threads.emptyThreads.message": "Any threads you are mentioned in or have participated in will show here along with any threads you have followed.",
|
||||
|
|
|
|||
1
index.ts
1
index.ts
|
|
@ -47,6 +47,7 @@ if (global.HermesInternal) {
|
|||
require('@formatjs/intl-numberformat/polyfill');
|
||||
require('@formatjs/intl-datetimeformat/polyfill');
|
||||
require('@formatjs/intl-datetimeformat/add-golden-tz');
|
||||
require('@formatjs/intl-listformat/polyfill');
|
||||
}
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
|
|
|
|||
101
package-lock.json
generated
101
package-lock.json
generated
|
|
@ -12,6 +12,7 @@
|
|||
"dependencies": {
|
||||
"@formatjs/intl-datetimeformat": "6.10.0",
|
||||
"@formatjs/intl-getcanonicallocales": "2.2.1",
|
||||
"@formatjs/intl-listformat": "7.5.0",
|
||||
"@formatjs/intl-locale": "3.3.2",
|
||||
"@formatjs/intl-numberformat": "8.7.0",
|
||||
"@formatjs/intl-pluralrules": "5.2.4",
|
||||
|
|
@ -2508,12 +2509,29 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl-listformat": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.4.0.tgz",
|
||||
"integrity": "sha512-ifupb+balZUAF/Oh3QyGRqPRWGSKwWoMPR0cYZEG7r61SimD+m38oFQqVx/3Fp7LfQFF11m7IS+MlxOo2sKINA==",
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.5.0.tgz",
|
||||
"integrity": "sha512-n9FsXGl1T2ZbX6wSyrzCDJHrbJR0YJ9ZNsAqUvHXfbY3nsOmGnSTf5+bkuIp1Xiywu7m1X1Pfm/Ngp/yK1H84A==",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "1.17.2",
|
||||
"@formatjs/intl-localematcher": "0.4.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl-listformat/node_modules/@formatjs/ecma402-abstract": {
|
||||
"version": "1.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.2.tgz",
|
||||
"integrity": "sha512-k2mTh0m+IV1HRdU0xXM617tSQTi53tVR2muvYOsBeYcUgEAyxV1FOC7Qj279th3fBVQ+Dj6muvNJZcHSPNdbKg==",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "0.4.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl-listformat/node_modules/@formatjs/intl-localematcher": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz",
|
||||
"integrity": "sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "1.17.0",
|
||||
"@formatjs/intl-localematcher": "0.4.0",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
|
|
@ -2566,6 +2584,16 @@
|
|||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl/node_modules/@formatjs/intl-listformat": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.4.0.tgz",
|
||||
"integrity": "sha512-ifupb+balZUAF/Oh3QyGRqPRWGSKwWoMPR0cYZEG7r61SimD+m38oFQqVx/3Fp7LfQFF11m7IS+MlxOo2sKINA==",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "1.17.0",
|
||||
"@formatjs/intl-localematcher": "0.4.0",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@gorhom/bottom-sheet": {
|
||||
"version": "4.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@gorhom/bottom-sheet/-/bottom-sheet-4.4.7.tgz",
|
||||
|
|
@ -19105,6 +19133,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-intl/node_modules/@formatjs/intl-listformat": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.4.0.tgz",
|
||||
"integrity": "sha512-ifupb+balZUAF/Oh3QyGRqPRWGSKwWoMPR0cYZEG7r61SimD+m38oFQqVx/3Fp7LfQFF11m7IS+MlxOo2sKINA==",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "1.17.0",
|
||||
"@formatjs/intl-localematcher": "0.4.0",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
|
|
@ -24666,6 +24704,18 @@
|
|||
"@formatjs/intl-listformat": "7.4.0",
|
||||
"intl-messageformat": "10.5.0",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formatjs/intl-listformat": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.4.0.tgz",
|
||||
"integrity": "sha512-ifupb+balZUAF/Oh3QyGRqPRWGSKwWoMPR0cYZEG7r61SimD+m38oFQqVx/3Fp7LfQFF11m7IS+MlxOo2sKINA==",
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.17.0",
|
||||
"@formatjs/intl-localematcher": "0.4.0",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@formatjs/intl-datetimeformat": {
|
||||
|
|
@ -24705,13 +24755,32 @@
|
|||
}
|
||||
},
|
||||
"@formatjs/intl-listformat": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.4.0.tgz",
|
||||
"integrity": "sha512-ifupb+balZUAF/Oh3QyGRqPRWGSKwWoMPR0cYZEG7r61SimD+m38oFQqVx/3Fp7LfQFF11m7IS+MlxOo2sKINA==",
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.5.0.tgz",
|
||||
"integrity": "sha512-n9FsXGl1T2ZbX6wSyrzCDJHrbJR0YJ9ZNsAqUvHXfbY3nsOmGnSTf5+bkuIp1Xiywu7m1X1Pfm/Ngp/yK1H84A==",
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.17.0",
|
||||
"@formatjs/intl-localematcher": "0.4.0",
|
||||
"@formatjs/ecma402-abstract": "1.17.2",
|
||||
"@formatjs/intl-localematcher": "0.4.2",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": {
|
||||
"version": "1.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.2.tgz",
|
||||
"integrity": "sha512-k2mTh0m+IV1HRdU0xXM617tSQTi53tVR2muvYOsBeYcUgEAyxV1FOC7Qj279th3fBVQ+Dj6muvNJZcHSPNdbKg==",
|
||||
"requires": {
|
||||
"@formatjs/intl-localematcher": "0.4.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"@formatjs/intl-localematcher": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz",
|
||||
"integrity": "sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==",
|
||||
"requires": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@formatjs/intl-locale": {
|
||||
|
|
@ -37055,6 +37124,18 @@
|
|||
"hoist-non-react-statics": "^3.3.2",
|
||||
"intl-messageformat": "10.5.0",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formatjs/intl-listformat": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.4.0.tgz",
|
||||
"integrity": "sha512-ifupb+balZUAF/Oh3QyGRqPRWGSKwWoMPR0cYZEG7r61SimD+m38oFQqVx/3Fp7LfQFF11m7IS+MlxOo2sKINA==",
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.17.0",
|
||||
"@formatjs/intl-localematcher": "0.4.0",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"dependencies": {
|
||||
"@formatjs/intl-datetimeformat": "6.10.0",
|
||||
"@formatjs/intl-getcanonicallocales": "2.2.1",
|
||||
"@formatjs/intl-listformat": "7.5.0",
|
||||
"@formatjs/intl-locale": "3.3.2",
|
||||
"@formatjs/intl-numberformat": "8.7.0",
|
||||
"@formatjs/intl-pluralrules": "5.2.4",
|
||||
|
|
|
|||
Loading…
Reference in a new issue