[Gekidou] channel quick actions (#6288)
* Add hitSlop to navigation header right buttons * Fix channel_item info muted style * Fix team switch when global threads * Wrap WS channel events in try/catch * Group Box component and Animated Group Box * SlideUpPanelItem style * Fix return value of setCurrentTeamAndChannelId * Add observeChannelSettings and include channel settings in prepareDeleteChannel * update OPTIONS_HEIGHT reference in find channels quick options * Fix DM limit in channel list * Fix category header style and translate default categories * Add snackbar for unmute/favorite/unfavorite * Add toggleFavoriteChannel remote action * Add makeDirectChannelVisible remote action * Use makeDirectChannelVisible in switchToChannelById and update toggleMuteChannel snackbar * Add channel actions common components * Update channel intro to use channel action common components * Rename ChannelDetails screen to ChannelInfo * Add channel quick actions * Update localization strings * Fix addChannelToDefaultCategory * Leave channel * Add localization strings * Fix snackBar screen event listener * Feedback review
This commit is contained in:
parent
06d2f1facd
commit
4573732fd2
57 changed files with 1628 additions and 479 deletions
|
|
@ -3,10 +3,16 @@
|
|||
|
||||
import {Model} from '@nozbe/watermelondb';
|
||||
|
||||
import {CHANNELS_CATEGORY, DMS_CATEGORY} from '@constants/categories';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {prepareCategories, prepareCategoryChannels, queryCategoriesByTeamIds, getCategoryById} from '@queries/servers/categories';
|
||||
import {getCurrentUserId} from '@queries/servers/system';
|
||||
import {queryMyTeams} from '@queries/servers/team';
|
||||
import {isDMorGM} from '@utils/channel';
|
||||
import {pluckUnique} from '@utils/helpers';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
||||
export const deleteCategory = async (serverUrl: string, categoryId: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
|
|
@ -109,3 +115,49 @@ export const toggleCollapseCategory = async (serverUrl: string, categoryId: stri
|
|||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export async function addChannelToDefaultCategory(serverUrl: string, channel: Channel | ChannelModel, prepareRecordsOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
|
||||
const teamId = 'teamId' in channel ? channel.teamId : channel.team_id;
|
||||
const userId = await getCurrentUserId(database);
|
||||
if (!userId) {
|
||||
return {error: 'no current user id'};
|
||||
}
|
||||
|
||||
const models: Model[] = [];
|
||||
const categoriesWithChannels: CategoryWithChannels[] = [];
|
||||
|
||||
if (isDMorGM(channel)) {
|
||||
const allTeamIds = await queryMyTeams(database).fetchIds();
|
||||
const categories = await queryCategoriesByTeamIds(database, allTeamIds).fetch();
|
||||
const channelCategories = categories.filter((c) => c.type === DMS_CATEGORY);
|
||||
for await (const cc of channelCategories) {
|
||||
const cwc = await cc.toCategoryWithChannels();
|
||||
cwc.channel_ids.unshift(channel.id);
|
||||
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);
|
||||
categoriesWithChannels.push(cwc);
|
||||
}
|
||||
|
||||
const ccModels = await prepareCategoryChannels(operator, categoriesWithChannels);
|
||||
models.push(...ccModels);
|
||||
}
|
||||
|
||||
if (models.length && !prepareRecordsOnly) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
return {models};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {Model} from '@nozbe/watermelondb';
|
|||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {General, Navigation as NavigationConstants, Preferences, Screens} from '@constants';
|
||||
import {CHANNELS_CATEGORY, DMS_CATEGORY} from '@constants/categories';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
|
|
@ -17,12 +16,10 @@ import {
|
|||
} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {prepareCommonSystemValues, PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId} from '@queries/servers/system';
|
||||
import {addChannelToTeamHistory, addTeamToTeamHistory, getTeamById, queryMyTeams, removeChannelFromTeamHistory} from '@queries/servers/team';
|
||||
import {addChannelToTeamHistory, addTeamToTeamHistory, getTeamById, removeChannelFromTeamHistory} from '@queries/servers/team';
|
||||
import {getCurrentUser, queryUsersById} from '@queries/servers/user';
|
||||
import {dismissAllModalsAndPopToRoot, dismissAllModalsAndPopToScreen} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {makeCategoryChannelId, makeCategoryId} from '@utils/categories';
|
||||
import {isDMorGM} from '@utils/channel';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {setThemeDefaults, updateThemeIfNeeded} from '@utils/theme';
|
||||
import {displayGroupMessageName, displayUsername, getUserIdFromChannelName} from '@utils/user';
|
||||
|
|
@ -428,54 +425,6 @@ export async function updateChannelsDisplayName(serverUrl: string, channels: Cha
|
|||
return {models};
|
||||
}
|
||||
|
||||
export async function addChannelToDefaultCategory(serverUrl: string, channel: Channel | ChannelModel, prepareRecordsOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
|
||||
const teamId = 'teamId' in channel ? channel.teamId : channel.team_id;
|
||||
const userId = await getCurrentUserId(database);
|
||||
if (!userId) {
|
||||
return {error: 'no current user id'};
|
||||
}
|
||||
|
||||
if (!isDMorGM(channel)) {
|
||||
const models = await operator.handleCategoryChannels({categoryChannels: [{
|
||||
category_id: makeCategoryId(CHANNELS_CATEGORY, userId, teamId),
|
||||
channel_id: channel.id,
|
||||
sort_order: 0,
|
||||
id: makeCategoryChannelId(teamId, channel.id),
|
||||
}],
|
||||
prepareRecordsOnly});
|
||||
|
||||
return {models};
|
||||
}
|
||||
|
||||
const allTeams = await queryMyTeams(database).fetch();
|
||||
const models = (
|
||||
await Promise.all(
|
||||
allTeams.map(
|
||||
(t) => operator.handleCategoryChannels({categoryChannels: [{
|
||||
category_id: makeCategoryId(DMS_CATEGORY, userId, t.id),
|
||||
channel_id: channel.id,
|
||||
sort_order: 0,
|
||||
id: makeCategoryChannelId(t.id, channel.id),
|
||||
}],
|
||||
prepareRecordsOnly: true}),
|
||||
),
|
||||
)
|
||||
).flat();
|
||||
|
||||
if (models.length && !prepareRecordsOnly) {
|
||||
operator.batchRecords(models);
|
||||
}
|
||||
|
||||
return {models};
|
||||
}
|
||||
|
||||
export async function showUnreadChannelsOnly(serverUrl: string, onlyUnreads: boolean) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
|
|
|
|||
|
|
@ -38,18 +38,20 @@ export const switchToGlobalThreads = async (serverUrl: string, teamId?: string,
|
|||
}
|
||||
|
||||
try {
|
||||
await setCurrentTeamAndChannelId(operator, teamId, '');
|
||||
await setCurrentTeamAndChannelId(operator, teamIdToUse, '');
|
||||
const history = await addChannelToTeamHistory(operator, teamIdToUse, Screens.GLOBAL_THREADS, true);
|
||||
models.push(...history);
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
const isTabletDevice = await isTablet();
|
||||
if (isTabletDevice) {
|
||||
DeviceEventEmitter.emit(Navigation.NAVIGATION_HOME, Screens.GLOBAL_THREADS);
|
||||
} else {
|
||||
goToScreen(Screens.GLOBAL_THREADS, '', {}, {topBar: {visible: false}});
|
||||
}
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,14 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {storeCategories} from '@actions/local/category';
|
||||
import {General} from '@constants';
|
||||
import {CHANNELS_CATEGORY, DMS_CATEGORY, FAVORITES_CATEGORY} from '@constants/categories';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getChannelCategory, queryCategoriesByTeamIds} from '@queries/servers/categories';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {getCurrentTeamId} from '@queries/servers/system';
|
||||
import {showFavoriteChannelSnackbar} from '@utils/snack_bar';
|
||||
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
|
|
@ -34,3 +41,75 @@ export const fetchCategories = async (serverUrl: string, teamId: string, prune =
|
|||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleFavoriteChannel = async (serverUrl: string, channelId: string, showSnackBar = false) => {
|
||||
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 {database} = operator;
|
||||
const channel = await getChannelById(database, channelId);
|
||||
if (!channel) {
|
||||
return {error: 'channel not found'};
|
||||
}
|
||||
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const teamId = channel?.teamId || currentTeamId;
|
||||
const currentCategory = await getChannelCategory(database, teamId, channelId);
|
||||
|
||||
if (!currentCategory) {
|
||||
return {error: 'channel does not belong to a category'};
|
||||
}
|
||||
|
||||
const categories = await queryCategoriesByTeamIds(database, [teamId]).fetch();
|
||||
const isFavorited = currentCategory.type === FAVORITES_CATEGORY;
|
||||
let targetWithChannels: CategoryWithChannels;
|
||||
let favoriteWithChannels: CategoryWithChannels;
|
||||
|
||||
if (isFavorited) {
|
||||
const categoryType = (channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL) ? DMS_CATEGORY : CHANNELS_CATEGORY;
|
||||
const targetCategory = categories.find((c) => c.type === categoryType);
|
||||
if (!targetCategory) {
|
||||
return {error: 'target category not found'};
|
||||
}
|
||||
targetWithChannels = await targetCategory.toCategoryWithChannels();
|
||||
targetWithChannels.channel_ids.unshift(channelId);
|
||||
|
||||
favoriteWithChannels = await currentCategory.toCategoryWithChannels();
|
||||
const channelIndex = favoriteWithChannels.channel_ids.indexOf(channelId);
|
||||
favoriteWithChannels.channel_ids.splice(channelIndex, 1);
|
||||
} else {
|
||||
const favoritesCategory = categories.find((c) => c.type === FAVORITES_CATEGORY);
|
||||
if (!favoritesCategory) {
|
||||
return {error: 'No favorites category'};
|
||||
}
|
||||
favoriteWithChannels = await favoritesCategory.toCategoryWithChannels();
|
||||
favoriteWithChannels.channel_ids.unshift(channelId);
|
||||
|
||||
targetWithChannels = await currentCategory.toCategoryWithChannels();
|
||||
const channelIndex = targetWithChannels.channel_ids.indexOf(channelId);
|
||||
targetWithChannels.channel_ids.splice(channelIndex, 1);
|
||||
}
|
||||
|
||||
await client.updateChannelCategories('me', teamId, [targetWithChannels, favoriteWithChannels]);
|
||||
|
||||
if (showSnackBar) {
|
||||
const onUndo = () => toggleFavoriteChannel(serverUrl, channelId, false);
|
||||
showFavoriteChannelSnackbar(!isFavorited, onUndo);
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
import {Model} from '@nozbe/watermelondb';
|
||||
import {IntlShape} from 'react-intl';
|
||||
|
||||
import {storeCategories} from '@actions/local/category';
|
||||
import {addChannelToDefaultCategory, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
|
||||
import {addChannelToDefaultCategory, storeCategories} from '@actions/local/category';
|
||||
import {removeCurrentUserFromChannel, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
|
||||
import {switchToGlobalThreads} from '@actions/local/thread';
|
||||
import {General, Preferences, Screens} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
|
|
@ -15,20 +15,22 @@ import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
|||
import NetworkManager from '@managers/network_manager';
|
||||
import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo, queryMyChannelSettingsByIds} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {getCommonSystemValues, getCurrentTeamId, getCurrentUserId} from '@queries/servers/system';
|
||||
import {getCommonSystemValues, getCurrentTeamId, getCurrentUserId, setCurrentChannelId} from '@queries/servers/system';
|
||||
import {prepareMyTeams, getNthLastChannelFromTeam, getMyTeamById, getTeamById, getTeamByName, queryMyTeams} from '@queries/servers/team';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {generateChannelNameFromDisplayName, getDirectChannelName, isDMorGM} from '@utils/channel';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {showMuteChannelSnackbar} from '@utils/snack_bar';
|
||||
import {PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url';
|
||||
import {displayGroupMessageName, displayUsername} from '@utils/user';
|
||||
|
||||
import {fetchPostsForChannel} from './post';
|
||||
import {setDirectChannelVisible} from './preference';
|
||||
import {fetchRolesIfNeeded} from './role';
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
import {addUserToTeam, fetchTeamByName, removeUserFromTeam} from './team';
|
||||
import {fetchProfilesPerChannels, fetchUsersByIds} from './user';
|
||||
import {fetchProfilesPerChannels, fetchUsersByIds, updateUsersNoLongerVisible} from './user';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
|
@ -157,6 +159,7 @@ export async function createChannel(serverUrl: string, displayName: string, purp
|
|||
return {channel: channelData};
|
||||
} catch (error) {
|
||||
EphemeralStore.creatingChannel = false;
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
|
@ -198,10 +201,65 @@ export async function patchChannel(serverUrl: string, channelPatch: Partial<Chan
|
|||
}
|
||||
return {channel: channelData};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function leaveChannel(serverUrl: string, channelId: 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 {database} = operator;
|
||||
const isTabletDevice = await isTablet();
|
||||
const user = await getCurrentUser(database);
|
||||
const models: Model[] = [];
|
||||
|
||||
if (!user) {
|
||||
return {error: 'current user not found'};
|
||||
}
|
||||
|
||||
EphemeralStore.addLeavingChannel(channelId);
|
||||
await client.removeFromChannel(user.id, channelId);
|
||||
|
||||
if (user.isGuest) {
|
||||
const {models: updateVisibleModels} = await updateUsersNoLongerVisible(serverUrl, true);
|
||||
if (updateVisibleModels) {
|
||||
models.push(...updateVisibleModels);
|
||||
}
|
||||
}
|
||||
|
||||
const {models: removeUserModels} = await removeCurrentUserFromChannel(serverUrl, channelId, true);
|
||||
if (removeUserModels) {
|
||||
models.push(...removeUserModels);
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
|
||||
if (isTabletDevice) {
|
||||
switchToLastChannel(serverUrl);
|
||||
} else {
|
||||
setCurrentChannelId(operator, '');
|
||||
}
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
} finally {
|
||||
EphemeralStore.removeLeavingChannel(channelId);
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchChannelCreator(serverUrl: string, channelId: string, fetchOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
|
|
@ -456,7 +514,7 @@ export async function joinChannel(serverUrl: string, userId: string, teamId: str
|
|||
}
|
||||
} catch (error) {
|
||||
if (channelId || channel?.id) {
|
||||
EphemeralStore.removeJoiningChanel(channelId || channel!.id);
|
||||
EphemeralStore.removeJoiningChannel(channelId || channel!.id);
|
||||
}
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
|
|
@ -486,13 +544,13 @@ export async function joinChannel(serverUrl: string, userId: string, teamId: str
|
|||
}
|
||||
} catch (error) {
|
||||
if (channelId || channel?.id) {
|
||||
EphemeralStore.removeJoiningChanel(channelId || channel!.id);
|
||||
EphemeralStore.removeJoiningChannel(channelId || channel!.id);
|
||||
}
|
||||
return {error};
|
||||
}
|
||||
|
||||
if (channelId || channel?.id) {
|
||||
EphemeralStore.removeJoiningChanel(channelId || channel!.id);
|
||||
EphemeralStore.removeJoiningChannel(channelId || channel!.id);
|
||||
}
|
||||
return {channel, member};
|
||||
}
|
||||
|
|
@ -810,6 +868,7 @@ export async function makeDirectChannel(serverUrl: string, userId: string, displ
|
|||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchArchivedChannels(serverUrl: string, teamId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE) {
|
||||
let client: Client;
|
||||
try {
|
||||
|
|
@ -901,6 +960,7 @@ export async function createGroupChannel(serverUrl: string, userIds: string[]) {
|
|||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchSharedChannels(serverUrl: string, teamId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE) {
|
||||
let client: Client;
|
||||
try {
|
||||
|
|
@ -938,6 +998,7 @@ export async function makeGroupChannel(serverUrl: string, userIds: string[], sho
|
|||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getChannelMemberCountsByGroup(serverUrl: string, channelId: string, includeTimezones: boolean) {
|
||||
let client: Client;
|
||||
try {
|
||||
|
|
@ -982,21 +1043,37 @@ export async function switchToChannelById(serverUrl: string, channelId: string,
|
|||
|
||||
fetchPostsForChannel(serverUrl, channelId);
|
||||
await switchToChannel(serverUrl, channelId, teamId, skipLastUnread);
|
||||
setDirectChannelVisible(serverUrl, channelId);
|
||||
markChannelAsRead(serverUrl, channelId);
|
||||
fetchChannelStats(serverUrl, channelId);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export async function switchToPenultimateChannel(serverUrl: string) {
|
||||
export async function switchToPenultimateChannel(serverUrl: string, teamId?: string) {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const currentTeam = await getCurrentTeamId(database);
|
||||
const channelId = await getNthLastChannelFromTeam(database, currentTeam, 1);
|
||||
const teamIdToUse = teamId || await getCurrentTeamId(database);
|
||||
const channelId = await getNthLastChannelFromTeam(database, teamIdToUse, 1);
|
||||
return switchToChannelById(serverUrl, channelId);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function switchToLastChannel(serverUrl: string, teamId?: string) {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const teamIdToUse = teamId || await getCurrentTeamId(database);
|
||||
const channelId = await getNthLastChannelFromTeam(database, teamIdToUse);
|
||||
return switchToChannelById(serverUrl, channelId);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
|
|
@ -1113,7 +1190,7 @@ export const toggleMuteChannel = async (serverUrl: string, channelId: string, sh
|
|||
|
||||
if (showSnackBar) {
|
||||
const onUndo = () => toggleMuteChannel(serverUrl, channelId, false);
|
||||
showMuteChannelSnackbar(onUndo);
|
||||
showMuteChannelSnackbar(mark_unread === 'mention', onUndo);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import {General, Preferences} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {getCurrentUserId} from '@queries/servers/system';
|
||||
import {getUserIdFromChannelName} from '@utils/user';
|
||||
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
const {CATEGORY_FAVORITE_CHANNEL, CATEGORY_SAVED_POST} = Preferences;
|
||||
const {CATEGORY_DIRECT_CHANNEL_SHOW, CATEGORY_GROUP_CHANNEL_SHOW, CATEGORY_FAVORITE_CHANNEL, CATEGORY_SAVED_POST} = Preferences;
|
||||
|
||||
export type MyPreferencesRequest = {
|
||||
preferences?: PreferenceType[];
|
||||
|
|
@ -149,3 +151,31 @@ export const deleteSavedPost = async (serverUrl: string, postId: string) => {
|
|||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const setDirectChannelVisible = async (serverUrl: string, channelId: string, visible = true) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const channel = await getChannelById(database, channelId);
|
||||
if (channel?.type === General.DM_CHANNEL || channel?.type === General.GM_CHANNEL) {
|
||||
const userId = await getCurrentUserId(database);
|
||||
const category = channel.type === General.DM_CHANNEL ? CATEGORY_DIRECT_CHANNEL_SHOW : CATEGORY_GROUP_CHANNEL_SHOW;
|
||||
const name = channel.type === General.DM_CHANNEL ? getUserIdFromChannelName(userId, channel.name) : channelId;
|
||||
const pref: PreferenceType = {
|
||||
user_id: userId,
|
||||
category,
|
||||
name,
|
||||
value: visible.toString(),
|
||||
};
|
||||
return savePreference(serverUrl, [pref]);
|
||||
}
|
||||
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,17 +4,13 @@
|
|||
import {Model} from '@nozbe/watermelondb';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {addChannelToDefaultCategory} from '@actions/local/category';
|
||||
import {
|
||||
addChannelToDefaultCategory,
|
||||
markChannelAsViewed,
|
||||
removeCurrentUserFromChannel,
|
||||
setChannelDeleteAt,
|
||||
storeMyChannelsForTeam,
|
||||
switchToChannel,
|
||||
updateChannelInfoFromChannel,
|
||||
updateMyChannelFromWebsocket} from '@actions/local/channel';
|
||||
markChannelAsViewed, removeCurrentUserFromChannel, setChannelDeleteAt,
|
||||
storeMyChannelsForTeam, updateChannelInfoFromChannel, updateMyChannelFromWebsocket,
|
||||
} from '@actions/local/channel';
|
||||
import {switchToGlobalThreads} from '@actions/local/thread';
|
||||
import {fetchMissingSidebarInfo, fetchMyChannel, fetchChannelStats, fetchChannelById} from '@actions/remote/channel';
|
||||
import {fetchMissingSidebarInfo, fetchMyChannel, fetchChannelStats, fetchChannelById, switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchPostsForChannel} from '@actions/remote/post';
|
||||
import {fetchRolesIfNeeded} from '@actions/remote/role';
|
||||
import {fetchUsersByIds, updateUsersNoLongerVisible} from '@actions/remote/user';
|
||||
|
|
@ -101,8 +97,8 @@ export async function handleChannelUpdatedEvent(serverUrl: string, msg: any) {
|
|||
return;
|
||||
}
|
||||
|
||||
const updatedChannel = JSON.parse(msg.data.channel);
|
||||
try {
|
||||
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) {
|
||||
|
|
@ -241,15 +237,14 @@ export async function handleUserAddedToChannelEvent(serverUrl: string, msg: any)
|
|||
return;
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
|
||||
const currentUser = await getCurrentUser(database);
|
||||
const userId = msg.data.user_id || msg.broadcast.userId;
|
||||
const channelId = msg.data.channel_id || msg.broadcast.channel_id;
|
||||
const {team_id: teamId} = msg.data;
|
||||
const models: Model[] = [];
|
||||
|
||||
try {
|
||||
const {database} = operator;
|
||||
const currentUser = await getCurrentUser(database);
|
||||
const userId = msg.data.user_id || msg.broadcast.userId;
|
||||
const channelId = msg.data.channel_id || msg.broadcast.channel_id;
|
||||
const {team_id: teamId} = msg.data;
|
||||
const models: Model[] = [];
|
||||
|
||||
if (userId === currentUser?.id) {
|
||||
if (EphemeralStore.isAddingToTeam(teamId) || EphemeralStore.isJoiningChannel(channelId)) {
|
||||
return;
|
||||
|
|
@ -318,76 +313,76 @@ export async function handleUserRemovedFromChannelEvent(serverUrl: string, msg:
|
|||
return;
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
try {
|
||||
// Depending on who was removed, the ids may come from one place dataset or the other.
|
||||
const userId = msg.data.user_id || msg.broadcast.user_id;
|
||||
const channelId = msg.data.channel_id || msg.broadcast.channel_id;
|
||||
|
||||
const channel = await getCurrentChannel(database);
|
||||
const user = await getCurrentUser(database);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Depending on who was removed, the ids may come from one place dataset or the other.
|
||||
const userId = msg.data.user_id || msg.broadcast.user_id;
|
||||
const channelId = msg.data.channel_id || msg.broadcast.channel_id;
|
||||
|
||||
const models: Model[] = [];
|
||||
|
||||
if (user.isGuest) {
|
||||
const {models: updateVisibleModels} = await updateUsersNoLongerVisible(serverUrl, true);
|
||||
if (updateVisibleModels) {
|
||||
models.push(...updateVisibleModels);
|
||||
}
|
||||
}
|
||||
|
||||
if (user.id === userId) {
|
||||
const {models: removeUserModels} = await removeCurrentUserFromChannel(serverUrl, channelId, true);
|
||||
if (removeUserModels) {
|
||||
models.push(...removeUserModels);
|
||||
if (EphemeralStore.isLeavingChannel(channelId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (channel && channel.id === channelId) {
|
||||
const currentServer = await queryActiveServer(DatabaseManager.appDatabase!.database);
|
||||
const {database} = operator;
|
||||
const channel = await getCurrentChannel(database);
|
||||
const user = await getCurrentUser(database);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentServer?.url === serverUrl) {
|
||||
DeviceEventEmitter.emit(Events.LEAVE_CHANNEL);
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
const models: Model[] = [];
|
||||
|
||||
if (await isTablet()) {
|
||||
let tId = channel.teamId;
|
||||
if (!tId) {
|
||||
tId = await getCurrentTeamId(database);
|
||||
}
|
||||
const channelToJumpTo = await getNthLastChannelFromTeam(database, tId);
|
||||
if (channelToJumpTo) {
|
||||
if (channelToJumpTo === Screens.GLOBAL_THREADS) {
|
||||
const {models: switchToGlobalThreadsModels} = await switchToGlobalThreads(serverUrl, tId, true);
|
||||
if (switchToGlobalThreadsModels) {
|
||||
models.push(...switchToGlobalThreadsModels);
|
||||
}
|
||||
} else {
|
||||
const {models: switchChannelModels} = await switchToChannel(serverUrl, channelToJumpTo, '', false, true);
|
||||
if (switchChannelModels) {
|
||||
models.push(...switchChannelModels);
|
||||
}
|
||||
if (user.isGuest) {
|
||||
const {models: updateVisibleModels} = await updateUsersNoLongerVisible(serverUrl, true);
|
||||
if (updateVisibleModels) {
|
||||
models.push(...updateVisibleModels);
|
||||
}
|
||||
}
|
||||
|
||||
if (user.id === userId) {
|
||||
await removeCurrentUserFromChannel(serverUrl, channelId);
|
||||
if (channel && channel.id === channelId) {
|
||||
const currentServer = await queryActiveServer(DatabaseManager.appDatabase!.database);
|
||||
|
||||
if (currentServer?.url === serverUrl) {
|
||||
DeviceEventEmitter.emit(Events.LEAVE_CHANNEL, channel.displayName);
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
|
||||
if (await isTablet()) {
|
||||
let tId = channel.teamId;
|
||||
if (!tId) {
|
||||
tId = await getCurrentTeamId(database);
|
||||
}
|
||||
const channelToJumpTo = await getNthLastChannelFromTeam(database, tId);
|
||||
if (channelToJumpTo) {
|
||||
if (channelToJumpTo === Screens.GLOBAL_THREADS) {
|
||||
const {models: switchToGlobalThreadsModels} = await switchToGlobalThreads(serverUrl, tId, true);
|
||||
if (switchToGlobalThreadsModels) {
|
||||
models.push(...switchToGlobalThreadsModels);
|
||||
}
|
||||
} else {
|
||||
switchToChannelById(serverUrl, channelToJumpTo, tId, true);
|
||||
}
|
||||
} // TODO else jump to "join a channel" screen https://mattermost.atlassian.net/browse/MM-41051
|
||||
} else {
|
||||
const currentChannelModels = await prepareCommonSystemValues(operator, {currentChannelId: ''});
|
||||
if (currentChannelModels?.length) {
|
||||
models.push(...currentChannelModels);
|
||||
}
|
||||
} // TODO else jump to "join a channel" screen https://mattermost.atlassian.net/browse/MM-41051
|
||||
} else {
|
||||
const currentChannelModels = await prepareCommonSystemValues(operator, {currentChannelId: ''});
|
||||
if (currentChannelModels?.length) {
|
||||
models.push(...currentChannelModels);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const {models: deleteMemberModels} = await deleteChannelMembership(operator, userId, channelId, true);
|
||||
if (deleteMemberModels) {
|
||||
models.push(...deleteMemberModels);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const {models: deleteMemberModels} = await deleteChannelMembership(operator, userId, channelId, true);
|
||||
if (deleteMemberModels) {
|
||||
models.push(...deleteMemberModels);
|
||||
}
|
||||
}
|
||||
|
||||
operator.batchRecords(models);
|
||||
operator.batchRecords(models);
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleChannelDeletedEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
|
|
@ -396,52 +391,58 @@ export async function handleChannelDeletedEvent(serverUrl: string, msg: WebSocke
|
|||
return;
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
try {
|
||||
const {database} = operator;
|
||||
const {channel_id: channelId, delete_at: deleteAt} = msg.data;
|
||||
if (EphemeralStore.isLeavingChannel(channelId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentChannel = await getCurrentChannel(database);
|
||||
const user = await getCurrentUser(database);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
const currentChannel = await getCurrentChannel(database);
|
||||
const user = await getCurrentUser(database);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {channel_id: channelId, delete_at: deleteAt} = msg.data;
|
||||
const config = await getConfig(database);
|
||||
|
||||
const config = await getConfig(database);
|
||||
await setChannelDeleteAt(serverUrl, channelId, deleteAt);
|
||||
|
||||
await setChannelDeleteAt(serverUrl, channelId, deleteAt);
|
||||
if (user.isGuest) {
|
||||
updateUsersNoLongerVisible(serverUrl);
|
||||
}
|
||||
|
||||
if (user.isGuest) {
|
||||
updateUsersNoLongerVisible(serverUrl);
|
||||
}
|
||||
if (config?.ExperimentalViewArchivedChannels !== 'true') {
|
||||
await removeCurrentUserFromChannel(serverUrl, channelId);
|
||||
|
||||
if (config?.ExperimentalViewArchivedChannels !== 'true') {
|
||||
removeCurrentUserFromChannel(serverUrl, channelId);
|
||||
if (currentChannel && currentChannel.id === channelId) {
|
||||
const currentServer = await queryActiveServer(DatabaseManager.appDatabase!.database);
|
||||
|
||||
if (currentChannel && currentChannel.id === channelId) {
|
||||
const currentServer = await queryActiveServer(DatabaseManager.appDatabase!.database);
|
||||
if (currentServer?.url === serverUrl) {
|
||||
DeviceEventEmitter.emit(Events.CHANNEL_ARCHIVED, currentChannel.displayName);
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
|
||||
if (currentServer?.url === serverUrl) {
|
||||
DeviceEventEmitter.emit(Events.CHANNEL_DELETED);
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
|
||||
if (await isTablet()) {
|
||||
let tId = currentChannel.teamId;
|
||||
if (!tId) {
|
||||
tId = await getCurrentTeamId(database);
|
||||
}
|
||||
const channelToJumpTo = await getNthLastChannelFromTeam(database, tId);
|
||||
if (channelToJumpTo) {
|
||||
if (channelToJumpTo === Screens.GLOBAL_THREADS) {
|
||||
switchToGlobalThreads(serverUrl, tId);
|
||||
return;
|
||||
if (await isTablet()) {
|
||||
let tId = currentChannel.teamId;
|
||||
if (!tId) {
|
||||
tId = await getCurrentTeamId(database);
|
||||
}
|
||||
switchToChannel(serverUrl, channelToJumpTo);
|
||||
} // TODO else jump to "join a channel" screen
|
||||
} else {
|
||||
setCurrentChannelId(operator, '');
|
||||
const channelToJumpTo = await getNthLastChannelFromTeam(database, tId);
|
||||
if (channelToJumpTo) {
|
||||
if (channelToJumpTo === Screens.GLOBAL_THREADS) {
|
||||
switchToGlobalThreads(serverUrl, tId);
|
||||
return;
|
||||
}
|
||||
switchToChannelById(serverUrl, channelToJumpTo, tId);
|
||||
} // TODO else jump to "join a channel" screen
|
||||
} else {
|
||||
setCurrentChannelId(operator, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export interface ClientCategoriesMix {
|
|||
getCategories: (userId: string, teamId: string) => Promise<CategoriesWithOrder>;
|
||||
getCategoriesOrder: (userId: string, teamId: string) => Promise<string[]>;
|
||||
getCategory: (userId: string, teamId: string, categoryId: string) => Promise<Category>;
|
||||
updateChannelCategories: (userId: string, teamId: string, categories: CategoryWithChannels[]) => Promise<CategoriesWithOrder>;
|
||||
}
|
||||
|
||||
const ClientCategories = (superclass: any) => class extends superclass {
|
||||
|
|
@ -26,6 +27,13 @@ const ClientCategories = (superclass: any) => class extends superclass {
|
|||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
updateChannelCategories = async (userId: string, teamId: string, categories: CategoryWithChannels[]) => {
|
||||
return this.doFetch(
|
||||
`${this.getCategoriesRoute(userId, teamId)}`,
|
||||
{method: 'put', body: categories},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientCategories;
|
||||
|
|
|
|||
38
app/components/channel_actions/add_people_box/index.tsx
Normal file
38
app/components/channel_actions/add_people_box/index.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// 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 {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
import OptionBox from '@components/option_box';
|
||||
import {Screens} from '@constants';
|
||||
import {dismissBottomSheet, showModal} from '@screens/navigation';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const AddPeopleBox = ({channelId, containerStyle, testID}: Props) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const onAddPeople = useCallback(async () => {
|
||||
await dismissBottomSheet();
|
||||
const title = intl.formatMessage({id: 'intro.add_people', defaultMessage: 'Add People'});
|
||||
showModal(Screens.CHANNEL_ADD_PEOPLE, title, {channelId});
|
||||
}, [intl, channelId]);
|
||||
|
||||
return (
|
||||
<OptionBox
|
||||
containerStyle={containerStyle}
|
||||
iconName='account-plus-outline'
|
||||
onPress={onAddPeople}
|
||||
testID={testID}
|
||||
text={intl.formatMessage({id: 'intro.add_people', defaultMessage: 'Add People'})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPeopleBox;
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Clipboard from '@react-native-community/clipboard';
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import AnimatedOptionBox from '@components/option_box/animated';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
|
||||
type Props = {
|
||||
channelName?: string;
|
||||
onAnimationEnd?: () => void;
|
||||
teamName?: string;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const CopyChannelLinkBox = ({channelName, onAnimationEnd, teamName, testID}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const onCopyLink = useCallback(() => {
|
||||
Clipboard.setString(`${serverUrl}/${teamName}/channels/${channelName}`);
|
||||
}, [channelName, teamName, serverUrl]);
|
||||
|
||||
return (
|
||||
<AnimatedOptionBox
|
||||
animatedBackgroundColor={theme.onlineIndicator}
|
||||
animatedColor={theme.buttonColor}
|
||||
animatedIconName='check'
|
||||
animatedText={intl.formatMessage({id: 'channel_info.copied', defaultMessage: 'Copied'})}
|
||||
iconName='link-variant'
|
||||
onAnimationEnd={onAnimationEnd}
|
||||
onPress={onCopyLink}
|
||||
testID={testID}
|
||||
text={intl.formatMessage({id: 'channel_info.copy_link', defaultMessage: 'Copy Link'})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyChannelLinkBox;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
import {observeTeam} from '@queries/servers/team';
|
||||
|
||||
import CopyChannelLinkBox from './copy_channel_link_box';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type OwnProps = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: OwnProps) => {
|
||||
const channel = observeChannel(database, channelId);
|
||||
const team = channel.pipe(
|
||||
switchMap((c) => (c?.teamId ? observeTeam(database, c.teamId) : of$(undefined))),
|
||||
);
|
||||
const teamName = team.pipe(
|
||||
switchMap((t) => of$(t?.name)),
|
||||
);
|
||||
|
||||
const channelName = channel.pipe(
|
||||
switchMap((c) => of$(c?.name)),
|
||||
);
|
||||
return {
|
||||
channelName,
|
||||
teamName,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(CopyChannelLinkBox));
|
||||
44
app/components/channel_actions/favorite_box/favorite_box.tsx
Normal file
44
app/components/channel_actions/favorite_box/favorite_box.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// 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 {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
import {toggleFavoriteChannel} from '@actions/remote/category';
|
||||
import OptionBox from '@components/option_box';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
isFavorited: boolean;
|
||||
showSnackBar?: boolean;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const FavoriteBox = ({channelId, containerStyle, isFavorited, showSnackBar = false, testID}: Props) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const handleOnPress = useCallback(async () => {
|
||||
await dismissBottomSheet();
|
||||
toggleFavoriteChannel(serverUrl, channelId, showSnackBar);
|
||||
}, [serverUrl, channelId, showSnackBar]);
|
||||
|
||||
return (
|
||||
<OptionBox
|
||||
activeIconName='star'
|
||||
activeText={intl.formatMessage({id: 'channel_info.favorited', defaultMessage: 'Favorited'})}
|
||||
containerStyle={containerStyle}
|
||||
iconName='star-outline'
|
||||
isActive={isFavorited}
|
||||
onPress={handleOnPress}
|
||||
testID={testID}
|
||||
text={intl.formatMessage({id: 'channel_info.favorite', defaultMessage: 'Favorite'})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default FavoriteBox;
|
||||
36
app/components/channel_actions/favorite_box/index.ts
Normal file
36
app/components/channel_actions/favorite_box/index.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {combineLatestWith, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeIsChannelFavorited} from '@queries/servers/categories';
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
import {observeCurrentTeamId} from '@queries/servers/system';
|
||||
|
||||
import FavoriteBox from './favorite_box';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type OwnProps = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: OwnProps) => {
|
||||
const currentTeamId = observeCurrentTeamId(database);
|
||||
const channel = observeChannel(database, channelId);
|
||||
const isFavorited = channel.pipe(
|
||||
combineLatestWith(currentTeamId),
|
||||
switchMap(([c, tId]) => observeIsChannelFavorited(database, c?.teamId || tId, channelId)),
|
||||
);
|
||||
|
||||
return {
|
||||
isFavorited,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(FavoriteBox));
|
||||
51
app/components/channel_actions/info_box/index.tsx
Normal file
51
app/components/channel_actions/info_box/index.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// 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 {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
import OptionBox from '@components/option_box';
|
||||
import SlideUpPanelItem from '@components/slide_up_panel_item';
|
||||
import {Screens} from '@constants';
|
||||
import {dismissBottomSheet, showModal} from '@screens/navigation';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
showAsLabel?: boolean;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const InfoBox = ({channelId, containerStyle, showAsLabel = false, testID}: Props) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const onViewInfo = useCallback(async () => {
|
||||
await dismissBottomSheet();
|
||||
const title = intl.formatMessage({id: 'screens.channel_info', defaultMessage: 'Channel Info'});
|
||||
showModal(Screens.CHANNEL_INFO, title, {channelId});
|
||||
}, [intl, channelId]);
|
||||
|
||||
if (showAsLabel) {
|
||||
return (
|
||||
<SlideUpPanelItem
|
||||
icon='information-outline'
|
||||
onPress={onViewInfo}
|
||||
testID={testID}
|
||||
text={intl.formatMessage({id: 'channel_header.info', defaultMessage: 'View info'})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<OptionBox
|
||||
containerStyle={containerStyle}
|
||||
iconName='information-outline'
|
||||
onPress={onViewInfo}
|
||||
testID={testID}
|
||||
text={intl.formatMessage({id: 'intro.channel_info', defaultMessage: 'Info'})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfoBox;
|
||||
37
app/components/channel_actions/leave_channel_label/index.ts
Normal file
37
app/components/channel_actions/leave_channel_label/index.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
|
||||
import LeaveChannelLabel from './leave_channel_label';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type OwnProps = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: OwnProps) => {
|
||||
const channel = observeChannel(database, channelId);
|
||||
const displayName = channel.pipe(
|
||||
switchMap((c) => of$(c?.displayName)),
|
||||
);
|
||||
const type = channel.pipe(
|
||||
switchMap((c) => of$(c?.type)),
|
||||
);
|
||||
|
||||
return {
|
||||
displayName,
|
||||
type,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(LeaveChannelLabel));
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
// 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 {Alert} from 'react-native';
|
||||
|
||||
import {leaveChannel} from '@actions/remote/channel';
|
||||
import {setDirectChannelVisible} from '@actions/remote/preference';
|
||||
import SlideUpPanelItem from '@components/slide_up_panel_item';
|
||||
import {General} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {dismissAllModals, dismissBottomSheet, popToRoot} from '@screens/navigation';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
displayName?: string;
|
||||
type?: string;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const LeaveChanelLabel = ({channelId, displayName, type, testID}: Props) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const isTablet = useIsTablet();
|
||||
|
||||
const close = async () => {
|
||||
await dismissBottomSheet();
|
||||
if (!isTablet) {
|
||||
await dismissAllModals();
|
||||
popToRoot();
|
||||
}
|
||||
};
|
||||
|
||||
const closeDirectMessage = () => {
|
||||
Alert.alert(
|
||||
intl.formatMessage({id: 'channel_info.close_dm', defaultMessage: 'Close direct message'}),
|
||||
intl.formatMessage({
|
||||
id: 'channel_info.close_dm_channel',
|
||||
defaultMessage: 'Are you sure you want to close this direct message? This will remove it from your home screen, but you can always open it again.',
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}),
|
||||
style: 'cancel',
|
||||
}, {
|
||||
text: intl.formatMessage({id: 'channel_info.close', defaultMessage: 'Close'}),
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
setDirectChannelVisible(serverUrl, channelId, false);
|
||||
close();
|
||||
},
|
||||
}], {cancelable: false},
|
||||
);
|
||||
};
|
||||
|
||||
const closeGroupMessage = () => {
|
||||
Alert.alert(
|
||||
intl.formatMessage({id: 'channel_info.close_gm', defaultMessage: 'Close group message'}),
|
||||
intl.formatMessage({
|
||||
id: 'channel_info.close_gm_channel',
|
||||
defaultMessage: '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.',
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}),
|
||||
style: 'cancel',
|
||||
}, {
|
||||
text: intl.formatMessage({id: 'channel_info.close', defaultMessage: 'Close'}),
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
setDirectChannelVisible(serverUrl, channelId, false);
|
||||
close();
|
||||
},
|
||||
}], {cancelable: false},
|
||||
);
|
||||
};
|
||||
|
||||
const leavePublicChannel = () => {
|
||||
Alert.alert(
|
||||
intl.formatMessage({id: 'channel_info.leave_channel', defaultMessage: 'Leave Channel'}),
|
||||
intl.formatMessage({
|
||||
id: 'channel_info.leave_public_channel',
|
||||
defaultMessage: 'Are you sure you want to leave the public channel {displayName}? You can always rejoin.',
|
||||
}, {displayName}),
|
||||
[{
|
||||
text: intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}),
|
||||
style: 'cancel',
|
||||
}, {
|
||||
text: intl.formatMessage({id: 'channel_info.leave', defaultMessage: 'Leave'}),
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
leaveChannel(serverUrl, channelId);
|
||||
close();
|
||||
},
|
||||
}], {cancelable: false},
|
||||
);
|
||||
};
|
||||
|
||||
const leavePrivateChannel = () => {
|
||||
Alert.alert(
|
||||
intl.formatMessage({id: 'channel_info.leave_channel', defaultMessage: 'Leave Channel'}),
|
||||
intl.formatMessage({
|
||||
id: 'channel_info.leave_private_channel',
|
||||
defaultMessage: "Are you sure you want to leave the private channel {displayName}? You cannot rejoin the channel unless you're invited again.",
|
||||
}, {displayName}),
|
||||
[{
|
||||
text: intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}),
|
||||
style: 'cancel',
|
||||
}, {
|
||||
text: intl.formatMessage({id: 'channel_info.leave', defaultMessage: 'Leave'}),
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
leaveChannel(serverUrl, channelId);
|
||||
close();
|
||||
},
|
||||
}], {cancelable: false},
|
||||
);
|
||||
};
|
||||
|
||||
const onLeave = () => {
|
||||
switch (type) {
|
||||
case General.OPEN_CHANNEL:
|
||||
leavePublicChannel();
|
||||
break;
|
||||
case General.PRIVATE_CHANNEL:
|
||||
leavePrivateChannel();
|
||||
break;
|
||||
case General.DM_CHANNEL:
|
||||
closeDirectMessage();
|
||||
break;
|
||||
case General.GM_CHANNEL:
|
||||
closeGroupMessage();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if (!displayName || !type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let leaveText = '';
|
||||
switch (type) {
|
||||
case General.DM_CHANNEL:
|
||||
leaveText = intl.formatMessage({id: 'channel_info.close_dm', defaultMessage: 'Close direct message'});
|
||||
break;
|
||||
case General.GM_CHANNEL:
|
||||
leaveText = intl.formatMessage({id: 'channel_info.close_gm', defaultMessage: 'Close group message'});
|
||||
break;
|
||||
default:
|
||||
leaveText = intl.formatMessage({id: 'channel_info.leave_channel', defaultMessage: 'Leave channel'});
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<SlideUpPanelItem
|
||||
destructive={true}
|
||||
icon='close'
|
||||
onPress={onLeave}
|
||||
text={leaveText}
|
||||
testID={testID}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LeaveChanelLabel;
|
||||
29
app/components/channel_actions/mute_box/index.ts
Normal file
29
app/components/channel_actions/mute_box/index.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {General} from '@constants';
|
||||
import {observeChannelSettings} from '@queries/servers/channel';
|
||||
|
||||
import MuteBox from './mute_box';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type OwnProps = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: OwnProps) => ({
|
||||
isMuted: observeChannelSettings(database, channelId).pipe(
|
||||
switchMap((s) => of$(s?.notifyProps?.mark_unread === General.MENTION)),
|
||||
),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhanced(MuteBox));
|
||||
44
app/components/channel_actions/mute_box/mute_box.tsx
Normal file
44
app/components/channel_actions/mute_box/mute_box.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// 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 {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
import {toggleMuteChannel} from '@actions/remote/channel';
|
||||
import OptionBox from '@components/option_box';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
isMuted: boolean;
|
||||
showSnackBar?: boolean;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const MutedBox = ({channelId, containerStyle, isMuted, showSnackBar = false, testID}: Props) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const handleOnPress = useCallback(async () => {
|
||||
await dismissBottomSheet();
|
||||
toggleMuteChannel(serverUrl, channelId, showSnackBar);
|
||||
}, [channelId, isMuted, serverUrl, showSnackBar]);
|
||||
|
||||
return (
|
||||
<OptionBox
|
||||
activeIconName='bell-off-outline'
|
||||
activeText={intl.formatMessage({id: 'channel_info.muted', defaultMessage: 'Muted'})}
|
||||
containerStyle={containerStyle}
|
||||
iconName='bell-outline'
|
||||
isActive={isMuted}
|
||||
onPress={handleOnPress}
|
||||
testID={testID}
|
||||
text={intl.formatMessage({id: 'channel_info.muted', defaultMessage: 'Mute'})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default MutedBox;
|
||||
33
app/components/channel_actions/set_header_box/index.ts
Normal file
33
app/components/channel_actions/set_header_box/index.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeChannelInfo} from '@queries/servers/channel';
|
||||
|
||||
import SetHeaderBox from './set_header';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type OwnProps = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: OwnProps) => {
|
||||
const channelInfo = observeChannelInfo(database, channelId);
|
||||
const isHeaderSet = channelInfo.pipe(
|
||||
switchMap((c) => of$(Boolean(c?.header))),
|
||||
);
|
||||
|
||||
return {
|
||||
isHeaderSet,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(SetHeaderBox));
|
||||
46
app/components/channel_actions/set_header_box/set_header.tsx
Normal file
46
app/components/channel_actions/set_header_box/set_header.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// 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 {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
import OptionBox from '@components/option_box';
|
||||
import {Screens} from '@constants';
|
||||
import {dismissBottomSheet, showModal} from '@screens/navigation';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
isHeaderSet: boolean;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const SetHeaderBox = ({channelId, containerStyle, isHeaderSet, testID}: Props) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const onSetHeader = useCallback(async () => {
|
||||
await dismissBottomSheet();
|
||||
const title = intl.formatMessage({id: 'screens.channel_edit_header', defaultMessage: 'Edit Channel Header'});
|
||||
showModal(Screens.CREATE_OR_EDIT_CHANNEL, title, {channelId, headerOnly: true});
|
||||
}, [intl, channelId]);
|
||||
|
||||
let text;
|
||||
if (isHeaderSet) {
|
||||
text = intl.formatMessage({id: 'channel_info.edit_header', defaultMessage: 'Edit Header'});
|
||||
} else {
|
||||
text = intl.formatMessage({id: 'channel_info.set_header', defaultMessage: 'Set Header'});
|
||||
}
|
||||
|
||||
return (
|
||||
<OptionBox
|
||||
containerStyle={containerStyle}
|
||||
iconName='pencil-outline'
|
||||
onPress={onSetHeader}
|
||||
testID={testID}
|
||||
text={text}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetHeaderBox;
|
||||
|
|
@ -151,7 +151,7 @@ const ChannelListItem = ({
|
|||
isActive && isTablet && !isInfo ? styles.textActive : null,
|
||||
isInfo ? styles.textInfo : null,
|
||||
isMuted && styles.muted,
|
||||
isMuted && isInfo && styles.infoMuted,
|
||||
isMuted && isInfo && styles.mutedInfo,
|
||||
], [isBolded, styles, isMuted, isActive, isInfo, isTablet]);
|
||||
|
||||
const containerStyle = useMemo(() => [
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ type EnhanceProps = WithDatabaseArgs & {
|
|||
showTeamName?: boolean;
|
||||
}
|
||||
|
||||
const observeIsMutedSetting = (mc: MyChannelModel) => mc.settings.observe().pipe(switchMap((s) => of$(s?.notifyProps?.mark_unread === 'mention')));
|
||||
const observeIsMutedSetting = (mc: MyChannelModel) => mc.settings.observe().pipe(switchMap((s) => of$(s?.notifyProps?.mark_unread === General.MENTION)));
|
||||
|
||||
const enhance = withObservables(['channel', 'showTeamName'], ({channel, database, showTeamName}: EnhanceProps) => {
|
||||
const currentUserId = observeCurrentUserId(database);
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ type Props = {
|
|||
}
|
||||
|
||||
const hitSlop = {top: 20, bottom: 20, left: 20, right: 20};
|
||||
const rightButtonHitSlop = {top: 20, bottom: 5, left: 5, right: 5};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
centered: {
|
||||
|
|
@ -227,10 +228,11 @@ const Header = ({
|
|||
<TouchableWithFeedback
|
||||
key={r.iconName}
|
||||
borderlessRipple={r.borderless === undefined ? true : r.borderless}
|
||||
hitSlop={rightButtonHitSlop}
|
||||
onPress={r.onPress}
|
||||
rippleRadius={r.rippleRadius || 20}
|
||||
type={r.buttonType || Platform.select({android: 'native', default: 'opacity'})}
|
||||
style={i > 0 ? styles.rightIcon : undefined}
|
||||
style={i > 0 && styles.rightIcon}
|
||||
testID={r.testID}
|
||||
>
|
||||
<CompassIcon
|
||||
|
|
|
|||
158
app/components/option_box/animated.tsx
Normal file
158
app/components/option_box/animated.tsx
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {Pressable, StyleSheet, Text} from 'react-native';
|
||||
import Animated, {Easing, interpolate, interpolateColor, runOnJS, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import {OPTIONS_HEIGHT} from '.';
|
||||
|
||||
type OptionBoxProps = {
|
||||
animatedBackgroundColor: string;
|
||||
animatedColor: string;
|
||||
animatedIconName: string;
|
||||
animatedText: string;
|
||||
iconName: string;
|
||||
onAnimationEnd?: () => void;
|
||||
onPress: () => void;
|
||||
testID?: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),
|
||||
borderRadius: 4,
|
||||
flex: 1,
|
||||
maxHeight: OPTIONS_HEIGHT,
|
||||
minWidth: 80,
|
||||
},
|
||||
center: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
text: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
paddingHorizontal: 5,
|
||||
textTransform: 'capitalize',
|
||||
...typography('Body', 50, 'SemiBold'),
|
||||
},
|
||||
}));
|
||||
|
||||
const AnimatedOptionBox = ({
|
||||
animatedBackgroundColor, animatedColor, animatedIconName, animatedText,
|
||||
iconName, onAnimationEnd, onPress, testID, text,
|
||||
}: OptionBoxProps) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const [activated, setActivated] = useState(false);
|
||||
const animate = useSharedValue(0);
|
||||
|
||||
const handleOnPress = useCallback(() => {
|
||||
animate.value = withTiming(1, {duration: 150, easing: Easing.out(Easing.linear)});
|
||||
setActivated(true);
|
||||
onPress();
|
||||
}, [onPress]);
|
||||
|
||||
const backgroundStyle = useAnimatedStyle(() => ({
|
||||
backgroundColor: interpolateColor(
|
||||
animate.value,
|
||||
[0, 1],
|
||||
['transparent', animatedBackgroundColor],
|
||||
),
|
||||
}));
|
||||
|
||||
const optionStyle = useAnimatedStyle(() => ({
|
||||
opacity: interpolate(
|
||||
animate.value,
|
||||
[0, 1],
|
||||
[1, 0],
|
||||
),
|
||||
}));
|
||||
|
||||
const scaleStyle = useAnimatedStyle(() => ({
|
||||
transform: [{
|
||||
scale: interpolate(
|
||||
animate.value,
|
||||
[0, 1],
|
||||
[0.25, 1],
|
||||
),
|
||||
}],
|
||||
opacity: animate.value,
|
||||
}), [activated]);
|
||||
|
||||
useEffect(() => {
|
||||
let t: NodeJS.Timeout|undefined;
|
||||
const callback = () => {
|
||||
'worklet';
|
||||
if (onAnimationEnd) {
|
||||
runOnJS(onAnimationEnd)();
|
||||
}
|
||||
};
|
||||
|
||||
if (activated) {
|
||||
t = setTimeout(() => {
|
||||
setActivated(false);
|
||||
animate.value = withTiming(0, {duration: 150, easing: Easing.out(Easing.linear)}, callback);
|
||||
}, 1200);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (t) {
|
||||
clearTimeout(t);
|
||||
}
|
||||
};
|
||||
}, [activated, animate.value, onAnimationEnd]);
|
||||
|
||||
return (
|
||||
<AnimatedPressable
|
||||
onPress={handleOnPress}
|
||||
disabled={activated}
|
||||
testID={testID}
|
||||
>
|
||||
{({pressed}) => (
|
||||
<Animated.View style={[styles.container, pressed && {backgroundColor: changeOpacity(theme.buttonBg, 0.16)}]}>
|
||||
<Animated.View style={[styles.container, backgroundStyle]}>
|
||||
<Animated.View style={[StyleSheet.absoluteFill, styles.center, optionStyle]}>
|
||||
<CompassIcon
|
||||
color={pressed ? theme.buttonBg : changeOpacity(theme.centerChannelColor, 0.56)}
|
||||
name={iconName}
|
||||
size={24}
|
||||
/>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[styles.text, {color: pressed ? theme.buttonBg : changeOpacity(theme.centerChannelColor, 0.56)}]}
|
||||
testID={`${testID}.label`}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
<Animated.View style={[StyleSheet.absoluteFill, styles.center, scaleStyle]}>
|
||||
<CompassIcon
|
||||
color={animatedColor}
|
||||
name={animatedIconName}
|
||||
size={24}
|
||||
/>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[styles.text, {color: animatedColor}]}
|
||||
testID={`${testID}.animated`}
|
||||
>
|
||||
{animatedText}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
)}
|
||||
</AnimatedPressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnimatedOptionBox;
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Text, TextStyle, TouchableHighlight, ViewStyle} from 'react-native';
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {Pressable, PressableStateCallbackType, StyleProp, Text, ViewStyle} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {useTheme} from '@context/theme';
|
||||
|
|
@ -10,56 +10,92 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
|||
import {typography} from '@utils/typography';
|
||||
|
||||
type OptionBoxProps = {
|
||||
iconColor?: string;
|
||||
activeIconName?: string;
|
||||
activeText?: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
iconName: string;
|
||||
isActive?: boolean;
|
||||
onPress: () => void;
|
||||
testID?: string;
|
||||
text: string;
|
||||
textStyle?: TextStyle;
|
||||
style?: ViewStyle;
|
||||
underlayColor?: string;
|
||||
}
|
||||
|
||||
export const OPTIONS_HEIGHT = 62;
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),
|
||||
borderRadius: 4,
|
||||
flex: 1,
|
||||
maxHeight: 60,
|
||||
maxHeight: OPTIONS_HEIGHT,
|
||||
justifyContent: 'center',
|
||||
minWidth: 115,
|
||||
minWidth: 80,
|
||||
},
|
||||
text: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
paddingHorizontal: 5,
|
||||
textTransform: 'capitalize',
|
||||
...typography('Body', 50, 'SemiBold'),
|
||||
},
|
||||
}));
|
||||
|
||||
const OptionBox = ({iconColor, iconName, onPress, style, text, textStyle, underlayColor}: OptionBoxProps) => {
|
||||
const OptionBox = ({activeIconName, activeText, containerStyle, iconName, isActive, onPress, testID, text}: OptionBoxProps) => {
|
||||
const theme = useTheme();
|
||||
const [activated, setActivated] = useState(isActive);
|
||||
const styles = getStyleSheet(theme);
|
||||
const pressedStyle = useCallback(({pressed}: PressableStateCallbackType) => {
|
||||
const style = [styles.container, Boolean(containerStyle) && containerStyle];
|
||||
|
||||
if (activated) {
|
||||
style.push({
|
||||
backgroundColor: changeOpacity(theme.buttonBg, 0.08),
|
||||
});
|
||||
}
|
||||
|
||||
if (pressed) {
|
||||
style.push({
|
||||
backgroundColor: changeOpacity(theme.buttonBg, 0.16),
|
||||
});
|
||||
}
|
||||
|
||||
return style;
|
||||
}, [activated, containerStyle, theme]);
|
||||
|
||||
const handleOnPress = useCallback(() => {
|
||||
if (activeIconName || activeText) {
|
||||
setActivated(!activated);
|
||||
}
|
||||
onPress();
|
||||
}, [activated, activeIconName, activeText, onPress]);
|
||||
|
||||
useEffect(() => {
|
||||
setActivated(isActive);
|
||||
}, [isActive]);
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
onPress={onPress}
|
||||
style={[styles.container, style]}
|
||||
underlayColor={changeOpacity(theme.centerChannelColor, 0.32) || underlayColor}
|
||||
<Pressable
|
||||
onPress={handleOnPress}
|
||||
style={pressedStyle}
|
||||
testID={testID}
|
||||
>
|
||||
<>
|
||||
<CompassIcon
|
||||
color={iconColor || changeOpacity(theme.centerChannelColor, 0.56)}
|
||||
name={iconName}
|
||||
size={24}
|
||||
/>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[styles.text, textStyle]}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</>
|
||||
</TouchableHighlight>
|
||||
{({pressed}) => (
|
||||
<>
|
||||
<CompassIcon
|
||||
color={(pressed || activated) ? theme.buttonBg : changeOpacity(theme.centerChannelColor, 0.56)}
|
||||
name={activated && activeIconName ? activeIconName : iconName}
|
||||
size={24}
|
||||
/>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[styles.text, {color: (pressed || activated) ? theme.buttonBg : changeOpacity(theme.centerChannelColor, 0.56)}]}
|
||||
testID={`${testID}.label`}
|
||||
>
|
||||
{activated && activeText ? activeText : text}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {StyleProp, Text, TextStyle, View, ViewStyle} from 'react-native';
|
||||
import {StyleProp, Text, TextStyle, TouchableHighlight, View, ViewStyle} from 'react-native';
|
||||
import FastImage, {ImageStyle, Source} from 'react-native-fast-image';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
|
@ -26,14 +25,15 @@ type SlideUpPanelProps = {
|
|||
|
||||
export const ITEM_HEIGHT = 51;
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
height: ITEM_HEIGHT,
|
||||
width: '100%',
|
||||
marginHorizontal: -20,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
destructive: {
|
||||
color: '#D0021B',
|
||||
color: theme.dndIndicator,
|
||||
},
|
||||
row: {
|
||||
width: '100%',
|
||||
|
|
@ -49,7 +49,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|||
width: 18,
|
||||
},
|
||||
icon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
},
|
||||
textContainer: {
|
||||
justifyContent: 'center',
|
||||
|
|
@ -59,7 +59,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|||
},
|
||||
text: {
|
||||
color: theme.centerChannelColor,
|
||||
opacity: 0.9,
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
};
|
||||
|
|
@ -101,25 +100,24 @@ const SlideUpPanelItem = ({destructive, icon, imageStyles, onPress, testID, text
|
|||
}
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
<TouchableHighlight
|
||||
onPress={handleOnPress}
|
||||
style={style.container}
|
||||
testID={testID}
|
||||
type='native'
|
||||
underlayColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
|
||||
>
|
||||
<View style={style.row}>
|
||||
{Boolean(image) && !rightIcon &&
|
||||
<View style={iconStyle}>{image}</View>
|
||||
}
|
||||
<View style={style.textContainer}>
|
||||
<Text style={[style.text, destructive ? style.destructive : null, textStyles]}>{text}</Text>
|
||||
<Text style={[style.text, destructive && style.destructive, textStyles]}>{text}</Text>
|
||||
</View>
|
||||
{Boolean(image) && rightIcon &&
|
||||
<View style={iconStyle}>{image}</View>
|
||||
}
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@
|
|||
|
||||
export const CHANNELS_CATEGORY = 'channels';
|
||||
export const DMS_CATEGORY = 'direct_messages';
|
||||
export const FAVORITES_CATEGORY = 'favorites';
|
||||
|
||||
export default {
|
||||
CHANNELS_CATEGORY,
|
||||
DMS_CATEGORY,
|
||||
FAVORITES_CATEGORY,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import keyMirror from '@utils/key_mirror';
|
|||
|
||||
export default keyMirror({
|
||||
ACCOUNT_SELECT_TABLET_VIEW: null,
|
||||
CHANNEL_DELETED: null,
|
||||
CHANNEL_ARCHIVED: null,
|
||||
CLOSE_BOTTOM_SHEET: null,
|
||||
CONFIG_CHANGED: null,
|
||||
FETCHING_POSTS: null,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export const BOTTOM_SHEET = 'BottomSheet';
|
|||
export const BROWSE_CHANNELS = 'BrowseChannels';
|
||||
export const CHANNEL = 'Channel';
|
||||
export const CHANNEL_ADD_PEOPLE = 'ChannelAddPeople';
|
||||
export const CHANNEL_DETAILS = 'ChannelDetails';
|
||||
export const CHANNEL_INFO = 'ChannelInfo';
|
||||
export const CHANNEL_EDIT = 'ChannelEdit';
|
||||
export const CODE = 'Code';
|
||||
export const CREATE_DIRECT_MESSAGE = 'CreateDirectMessage';
|
||||
|
|
@ -57,8 +57,8 @@ export default {
|
|||
BROWSE_CHANNELS,
|
||||
CHANNEL,
|
||||
CHANNEL_ADD_PEOPLE,
|
||||
CHANNEL_DETAILS,
|
||||
CHANNEL_EDIT,
|
||||
CHANNEL_INFO,
|
||||
CODE,
|
||||
CREATE_DIRECT_MESSAGE,
|
||||
CREATE_OR_EDIT_CHANNEL,
|
||||
|
|
@ -101,7 +101,7 @@ export default {
|
|||
|
||||
export const MODAL_SCREENS_WITHOUT_BACK = [
|
||||
BROWSE_CHANNELS,
|
||||
CHANNEL_DETAILS,
|
||||
CHANNEL_INFO,
|
||||
CREATE_DIRECT_MESSAGE,
|
||||
CREATE_TEAM,
|
||||
CUSTOM_STATUS,
|
||||
|
|
@ -118,7 +118,7 @@ export const MODAL_SCREENS_WITHOUT_BACK = [
|
|||
|
||||
export const NOT_READY = [
|
||||
CHANNEL_ADD_PEOPLE,
|
||||
CHANNEL_DETAILS,
|
||||
CHANNEL_INFO,
|
||||
CREATE_TEAM,
|
||||
INTEGRATION_SELECTOR,
|
||||
INTERACTIVE_DIALOG,
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@ import {t} from '@i18n';
|
|||
import keyMirror from '@utils/key_mirror';
|
||||
|
||||
export const SNACK_BAR_TYPE = keyMirror({
|
||||
FAVORITE_CHANNEL: null,
|
||||
LINK_COPIED: null,
|
||||
MESSAGE_COPIED: null,
|
||||
MUTE_CHANNEL: null,
|
||||
UNFAVORITE_CHANNEL: null,
|
||||
UNMUTE_CHANNEL: null,
|
||||
});
|
||||
|
||||
type SnackBarConfig = {
|
||||
|
|
@ -16,7 +19,14 @@ type SnackBarConfig = {
|
|||
iconName: string;
|
||||
canUndo: boolean;
|
||||
};
|
||||
|
||||
export const SNACK_BAR_CONFIG: Record<string, SnackBarConfig> = {
|
||||
FAVORITE_CHANNEL: {
|
||||
id: t('snack.bar.favorited.channel'),
|
||||
defaultMessage: 'This channel was favorited',
|
||||
iconName: 'star',
|
||||
canUndo: true,
|
||||
},
|
||||
LINK_COPIED: {
|
||||
id: t('snack.bar.link.copied'),
|
||||
defaultMessage: 'Link copied to clipboard',
|
||||
|
|
@ -35,6 +45,18 @@ export const SNACK_BAR_CONFIG: Record<string, SnackBarConfig> = {
|
|||
iconName: 'bell-off-outline',
|
||||
canUndo: true,
|
||||
},
|
||||
UNFAVORITE_CHANNEL: {
|
||||
id: t('snack.bar.unfavorite.channel'),
|
||||
defaultMessage: 'This channel was unfavorited',
|
||||
iconName: 'star-outline',
|
||||
canUndo: true,
|
||||
},
|
||||
UNMUTE_CHANNEL: {
|
||||
id: t('snack.bar.unmute.channel'),
|
||||
defaultMessage: 'This channel was unmuted',
|
||||
iconName: 'bell-outline',
|
||||
canUndo: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -109,4 +109,23 @@ export default class CategoryModel extends Model implements CategoryInterface {
|
|||
map((c) => c > 0),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
toCategoryWithChannels = async (): Promise<CategoryWithChannels> => {
|
||||
const categoryChannels = await this.categoryChannels.fetch();
|
||||
const orderedChannelIds = categoryChannels.sort((a, b) => {
|
||||
return b.sortOrder - a.sortOrder;
|
||||
}).map((cc) => cc.channelId);
|
||||
|
||||
return {
|
||||
channel_ids: orderedChannelIds,
|
||||
id: this.id,
|
||||
team_id: this.teamId,
|
||||
display_name: this.displayName,
|
||||
sort_order: this.sortOrder,
|
||||
sorting: this.sorting,
|
||||
type: this.type,
|
||||
muted: this.muted,
|
||||
collapsed: this.collapsed,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database, Model, Q, Query} from '@nozbe/watermelondb';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
||||
|
||||
import {FAVORITES_CATEGORY} from '@constants/categories';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {makeCategoryChannelId} from '@utils/categories';
|
||||
|
||||
|
|
@ -10,7 +13,7 @@ import type ServerDataOperator from '@database/operator/server_data_operator';
|
|||
import type CategoryModel from '@typings/database/models/servers/category';
|
||||
import type CategoryChannelModel from '@typings/database/models/servers/category_channel';
|
||||
|
||||
const {SERVER: {CATEGORY}} = MM_TABLES;
|
||||
const {SERVER: {CATEGORY, CATEGORY_CHANNEL}} = MM_TABLES;
|
||||
|
||||
export const getCategoryById = async (database: Database, categoryId: string) => {
|
||||
try {
|
||||
|
|
@ -70,3 +73,34 @@ export const prepareDeleteCategory = async (category: CategoryModel): Promise<Mo
|
|||
|
||||
return preparedModels;
|
||||
};
|
||||
|
||||
export const queryChannelCategory = (database: Database, teamId: string, channelId: string) => {
|
||||
return database.get<CategoryModel>(CATEGORY).query(
|
||||
Q.on(CATEGORY_CHANNEL, Q.where('id', makeCategoryChannelId(teamId, channelId))),
|
||||
);
|
||||
};
|
||||
|
||||
export const getChannelCategory = async (database: Database, teamId: string, channelId: string) => {
|
||||
const result = await queryChannelCategory(database, teamId, channelId).fetch();
|
||||
if (result.length) {
|
||||
return result[0];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getIsChannelFavorited = async (database: Database, teamId: string, channelId: string) => {
|
||||
const result = await queryChannelCategory(database, teamId, channelId).fetch();
|
||||
if (result.length > 0) {
|
||||
return result[0].type === FAVORITES_CATEGORY;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const observeIsChannelFavorited = (database: Database, teamId: string, channelId: string) => {
|
||||
return queryChannelCategory(database, teamId, channelId).observe().pipe(
|
||||
switchMap((result) => (result.length ? of$(result[0].type === FAVORITES_CATEGORY) : of$(false))),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -127,12 +127,18 @@ export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, tea
|
|||
export const prepareDeleteChannel = async (channel: ChannelModel): Promise<Model[]> => {
|
||||
const preparedModels: Model[] = [channel.prepareDestroyPermanently()];
|
||||
|
||||
const relations: Array<Relation<Model> | undefined> = [channel.membership, channel.info, channel.categoryChannel];
|
||||
const relations: Array<Relation<Model|MyChannelModel> | undefined> = [channel.membership, channel.info, channel.categoryChannel];
|
||||
await Promise.all(relations.map(async (relation) => {
|
||||
try {
|
||||
const model = await relation?.fetch();
|
||||
if (model) {
|
||||
preparedModels.push(model.prepareDestroyPermanently());
|
||||
if ('settings' in model) {
|
||||
const settings = await model.settings?.fetch();
|
||||
if (settings) {
|
||||
preparedModels.push(settings.prepareDestroyPermanently());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Record not found, do nothing
|
||||
|
|
@ -551,3 +557,9 @@ export const observeArchiveChannelsByTerm = (database: Database, term: string, t
|
|||
Q.take(take),
|
||||
).observe();
|
||||
};
|
||||
|
||||
export const observeChannelSettings = (database: Database, channelId: string) => {
|
||||
return database.get<MyChannelSettingsModel>(MY_CHANNEL_SETTINGS).query(Q.where('id', channelId), Q.take(1)).observe().pipe(
|
||||
switchMap((result) => (result.length ? result[0].observe() : of$(undefined))),
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ export async function setCurrentTeamAndChannelId(operator: ServerDataOperator, t
|
|||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
return {currentChannelId: channelId};
|
||||
return {currentTeamId: teamId, currentChannelId: channelId};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,7 +147,6 @@ const DirectChannel = ({channel, currentUserId, isBot, members, theme}: Props) =
|
|||
header={true}
|
||||
favorite={true}
|
||||
people={false}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import type ChannelModel from '@typings/database/models/servers/channel';
|
|||
import type RoleModel from '@typings/database/models/servers/role';
|
||||
|
||||
type Props = {
|
||||
channel: ChannelModel;
|
||||
channel?: ChannelModel;
|
||||
hasPosts: boolean;
|
||||
roles: RoleModel[];
|
||||
}
|
||||
|
|
@ -39,6 +39,10 @@ const Intro = ({channel, hasPosts, roles}: Props) => {
|
|||
const [fetching, setFetching] = useState(!hasPosts);
|
||||
const theme = useTheme();
|
||||
const element = useMemo(() => {
|
||||
if (!channel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (channel.type === General.OPEN_CHANNEL && channel.name === General.DEFAULT_CHANNEL) {
|
||||
return (
|
||||
<TownSquare
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
// 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 {saveFavoriteChannel} from '@actions/remote/preference';
|
||||
import {useServerUrl} from '@context/server';
|
||||
|
||||
import OptionItem from '../item';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
isFavorite: boolean;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const IntroFavorite = ({channelId, isFavorite, theme}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const toggleFavorite = useCallback(() => {
|
||||
saveFavoriteChannel(serverUrl, channelId, !isFavorite);
|
||||
}, [channelId, isFavorite]);
|
||||
|
||||
return (
|
||||
<OptionItem
|
||||
applyMargin={true}
|
||||
color={isFavorite ? theme.buttonBg : undefined}
|
||||
iconName={isFavorite ? 'star' : 'star-outline'}
|
||||
label={formatMessage({id: 'intro.favorite', defaultMessage: 'Favorite'})}
|
||||
onPress={toggleFavorite}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntroFavorite;
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
|
||||
import FavoriteItem from './favorite';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({channelId, database}: {channelId: string} & WithDatabaseArgs) => ({
|
||||
isFavorite: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_FAVORITE_CHANNEL, channelId).observeWithColumns(['value']).pipe(
|
||||
switchMap((prefs) => {
|
||||
return prefs.length ? of$(prefs[0].value === 'true') : of$(false);
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhanced(FavoriteItem));
|
||||
|
|
@ -1,22 +1,19 @@
|
|||
// 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 React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import {Screens} from '@constants';
|
||||
import {showModal} from '@screens/navigation';
|
||||
|
||||
import IntroFavorite from './favorite';
|
||||
import OptionItem from './item';
|
||||
import AddPeopleBox from '@components/channel_actions/add_people_box';
|
||||
import FavoriteBox from '@components/channel_actions/favorite_box';
|
||||
import InfoBox from '@components/channel_actions/info_box';
|
||||
import SetHeaderBox from '@components/channel_actions/set_header_box';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
header?: boolean;
|
||||
favorite?: boolean;
|
||||
people?: boolean;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
|
@ -27,60 +24,49 @@ const styles = StyleSheet.create({
|
|||
marginTop: 28,
|
||||
width: '100%',
|
||||
},
|
||||
margin: {
|
||||
marginRight: 8,
|
||||
},
|
||||
item: {
|
||||
alignItems: 'center',
|
||||
borderRadius: 4,
|
||||
height: 70,
|
||||
justifyContent: 'center',
|
||||
maxHeight: undefined,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
width: 112,
|
||||
},
|
||||
});
|
||||
|
||||
const IntroOptions = ({channelId, header, favorite, people, theme}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const onAddPeople = useCallback(() => {
|
||||
const title = formatMessage({id: 'intro.add_people', defaultMessage: 'Add People'});
|
||||
showModal(Screens.CHANNEL_ADD_PEOPLE, title, {channelId});
|
||||
}, []);
|
||||
|
||||
const onSetHeader = useCallback(() => {
|
||||
const title = formatMessage({id: 'screens.channel_edit_header', defaultMessage: 'Edit Channel Header'});
|
||||
showModal(Screens.CREATE_OR_EDIT_CHANNEL, title, {channelId, headerOnly: true});
|
||||
}, []);
|
||||
|
||||
const onDetails = useCallback(() => {
|
||||
const title = formatMessage({id: 'screens.channel_details', defaultMessage: 'Channel Details'});
|
||||
showModal(Screens.CHANNEL_DETAILS, title, {channelId});
|
||||
}, []);
|
||||
|
||||
const IntroOptions = ({channelId, header, favorite, people}: Props) => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{people &&
|
||||
<OptionItem
|
||||
applyMargin={true}
|
||||
iconName='account-plus-outline'
|
||||
label={formatMessage({id: 'intro.add_people', defaultMessage: 'Add People'})}
|
||||
onPress={onAddPeople}
|
||||
<AddPeopleBox
|
||||
channelId={channelId}
|
||||
containerStyle={[styles.item, styles.margin]}
|
||||
testID='channel_post_list.intro.option_item.add_people'
|
||||
theme={theme}
|
||||
/>
|
||||
}
|
||||
{header &&
|
||||
<OptionItem
|
||||
applyMargin={true}
|
||||
iconName='pencil-outline'
|
||||
label={formatMessage({id: 'intro.set_header', defaultMessage: 'Set Header'})}
|
||||
onPress={onSetHeader}
|
||||
<SetHeaderBox
|
||||
channelId={channelId}
|
||||
containerStyle={[styles.item, styles.margin]}
|
||||
testID='channel_post_list.intro.option_item.set_header'
|
||||
theme={theme}
|
||||
/>
|
||||
}
|
||||
{favorite &&
|
||||
<IntroFavorite
|
||||
<FavoriteBox
|
||||
channelId={channelId}
|
||||
theme={theme}
|
||||
containerStyle={[styles.item, styles.margin]}
|
||||
testID='channel_post_list.intro.option_item.favorite'
|
||||
/>
|
||||
}
|
||||
<OptionItem
|
||||
iconName='information-outline'
|
||||
label={formatMessage({id: 'intro.channel_details', defaultMessage: 'Details'})}
|
||||
onPress={onDetails}
|
||||
<InfoBox
|
||||
channelId={channelId}
|
||||
containerStyle={styles.item}
|
||||
testID='channel_post_list.intro.option_item.channel_details'
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,84 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {Pressable, PressableStateCallbackType, Text} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
applyMargin?: boolean;
|
||||
color?: string;
|
||||
iconName: string;
|
||||
label: string;
|
||||
onPress: () => void;
|
||||
testID?: string;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),
|
||||
borderRadius: 4,
|
||||
height: 70,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
width: 112,
|
||||
},
|
||||
containerPressed: {
|
||||
backgroundColor: changeOpacity(theme.buttonBg, 0.08),
|
||||
},
|
||||
label: {
|
||||
marginTop: 6,
|
||||
...typography('Body', 50, 'SemiBold'),
|
||||
},
|
||||
margin: {
|
||||
marginRight: 8,
|
||||
},
|
||||
}));
|
||||
|
||||
const IntroItem = ({applyMargin, color, iconName, label, onPress, testID, theme}: Props) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
const pressedStyle = useCallback(({pressed}: PressableStateCallbackType) => {
|
||||
const style = [styles.container];
|
||||
if (pressed) {
|
||||
style.push(styles.containerPressed);
|
||||
}
|
||||
|
||||
if (applyMargin) {
|
||||
style.push(styles.margin);
|
||||
}
|
||||
|
||||
return style;
|
||||
}, [applyMargin, theme]);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
style={pressedStyle}
|
||||
testID={testID}
|
||||
>
|
||||
{({pressed}) => (
|
||||
<>
|
||||
<CompassIcon
|
||||
name={iconName}
|
||||
color={pressed ? theme.linkColor : color || changeOpacity(theme.centerChannelColor, 0.56)}
|
||||
size={24}
|
||||
/>
|
||||
<Text
|
||||
style={[styles.label, {color: pressed ? theme.linkColor : color || changeOpacity(theme.centerChannelColor, 0.56)}]}
|
||||
testID={`${testID}.label`}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntroItem;
|
||||
|
|
@ -140,7 +140,6 @@ const PublicOrPrivateChannel = ({channel, creator, roles, theme}: Props) => {
|
|||
channelId={channel.id}
|
||||
header={canSetHeader}
|
||||
people={canManagePeople}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -61,7 +61,6 @@ const TownSquare = ({channelId, displayName, roles, theme}: Props) => {
|
|||
<IntroOptions
|
||||
channelId={channelId}
|
||||
header={hasPermission(roles, Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES, false)}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,14 +8,15 @@ import {DeviceEventEmitter, Keyboard, Platform, Text, View} from 'react-native';
|
|||
import CompassIcon from '@components/compass_icon';
|
||||
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
|
||||
import NavigationHeader from '@components/navigation_header';
|
||||
import {Navigation} from '@constants';
|
||||
import {Navigation, Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {popTopScreen, showModal} from '@screens/navigation';
|
||||
import {bottomSheet, popTopScreen, showModal} from '@screens/navigation';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import OtherMentionsBadge from './other_mentions_badge';
|
||||
import ChannelQuickAction from './quick_actions';
|
||||
|
||||
import type {HeaderRightButton} from '@components/navigation_header/header';
|
||||
|
||||
|
|
@ -58,7 +59,7 @@ const ChannelHeader = ({
|
|||
isCustomStatusExpired, isOwnDirectMessage, memberCount,
|
||||
searchTerm, teamId,
|
||||
}: ChannelProps) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const intl = useIntl();
|
||||
const isTablet = useIsTablet();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
|
@ -71,6 +72,35 @@ const ChannelHeader = ({
|
|||
return (<OtherMentionsBadge channelId={channelId}/>);
|
||||
}, [isTablet, channelId, teamId]);
|
||||
|
||||
const onBackPress = useCallback(() => {
|
||||
Keyboard.dismiss();
|
||||
popTopScreen(componentId);
|
||||
}, []);
|
||||
|
||||
const onTitlePress = useCallback(() => {
|
||||
const title = intl.formatMessage({id: 'screens.channel_info', defaultMessage: 'Channel Info'});
|
||||
showModal(Screens.CHANNEL_INFO, title, {channelId});
|
||||
}, [channelId, intl]);
|
||||
|
||||
const onChannelQuickAction = useCallback(() => {
|
||||
if (isTablet) {
|
||||
onTitlePress();
|
||||
return;
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
return <ChannelQuickAction channelId={channelId}/>;
|
||||
};
|
||||
|
||||
bottomSheet({
|
||||
title: '',
|
||||
renderContent,
|
||||
snapPoints: ['32%', 10],
|
||||
theme,
|
||||
closeButtonId: 'close-channel-quick-actions',
|
||||
});
|
||||
}, [channelId, isTablet, onTitlePress, theme]);
|
||||
|
||||
const rightButtons: HeaderRightButton[] = useMemo(() => ([{
|
||||
iconName: 'magnify',
|
||||
onPress: () => {
|
||||
|
|
@ -81,31 +111,20 @@ const ChannelHeader = ({
|
|||
},
|
||||
}, {
|
||||
iconName: Platform.select({android: 'dots-vertical', default: 'dots-horizontal'}),
|
||||
onPress: () => true,
|
||||
onPress: onChannelQuickAction,
|
||||
buttonType: 'opacity',
|
||||
}]), [isTablet, searchTerm]);
|
||||
|
||||
const onBackPress = useCallback(() => {
|
||||
Keyboard.dismiss();
|
||||
popTopScreen(componentId);
|
||||
}, []);
|
||||
|
||||
const onTitlePress = useCallback(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Title Press go to Channel Info');
|
||||
showModal('ChannelInfo', '', {channelId});
|
||||
}, [channelId]);
|
||||
}]), [isTablet, searchTerm, onChannelQuickAction]);
|
||||
|
||||
let title = displayName;
|
||||
if (isOwnDirectMessage) {
|
||||
title = formatMessage({id: 'channel_header.directchannel.you', defaultMessage: '{displayName} (you)'}, {displayName});
|
||||
title = intl.formatMessage({id: 'channel_header.directchannel.you', defaultMessage: '{displayName} (you)'}, {displayName});
|
||||
}
|
||||
|
||||
let subtitle;
|
||||
if (memberCount) {
|
||||
subtitle = formatMessage({id: 'channel', defaultMessage: '{count, plural, one {# member} other {# members}}'}, {count: memberCount});
|
||||
subtitle = intl.formatMessage({id: 'channel_header.member_count', defaultMessage: '{count, plural, one {# member} other {# members}}'}, {count: memberCount});
|
||||
} else if (!customStatus || !customStatus.text || isCustomStatusExpired) {
|
||||
subtitle = formatMessage({id: 'channel.details', defaultMessage: 'View details'});
|
||||
subtitle = intl.formatMessage({id: 'channel_header.info', defaultMessage: 'View info'});
|
||||
}
|
||||
|
||||
const subtitleCompanion = useMemo(() => {
|
||||
|
|
|
|||
32
app/screens/channel/header/quick_actions/index.ts
Normal file
32
app/screens/channel/header/quick_actions/index.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
|
||||
import ChannelQuickAction from './quick_actions';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type OwnProps = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: OwnProps) => {
|
||||
const channelType = observeChannel(database, channelId).pipe(
|
||||
switchMap((c) => of$(c?.type)),
|
||||
);
|
||||
|
||||
return {
|
||||
channelType,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(ChannelQuickAction));
|
||||
87
app/screens/channel/header/quick_actions/quick_actions.tsx
Normal file
87
app/screens/channel/header/quick_actions/quick_actions.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import AddPeopleBox from '@app/components/channel_actions/add_people_box';
|
||||
import CopyChannelLinkBox from '@app/components/channel_actions/copy_channel_link_box';
|
||||
import FavoriteBox from '@components/channel_actions/favorite_box';
|
||||
import InfoBox from '@components/channel_actions/info_box';
|
||||
import LeaveChannelLabel from '@components/channel_actions/leave_channel_label';
|
||||
import MutedBox from '@components/channel_actions/mute_box';
|
||||
import SetHeaderBox from '@components/channel_actions/set_header_box';
|
||||
import {General} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
channelType?: string;
|
||||
}
|
||||
|
||||
const OPTIONS_HEIGHT = 62;
|
||||
const DIRECT_CHANNELS: string[] = [General.DM_CHANNEL, General.GM_CHANNEL];
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
minHeight: 270,
|
||||
},
|
||||
wrapper: {
|
||||
flexDirection: 'row',
|
||||
height: OPTIONS_HEIGHT,
|
||||
marginBottom: 8,
|
||||
},
|
||||
separator: {
|
||||
width: 8,
|
||||
},
|
||||
});
|
||||
|
||||
const ChannelQuickAction = ({channelId, channelType}: Props) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const onCopyLinkAnimationEnd = useCallback(() => {
|
||||
requestAnimationFrame(async () => {
|
||||
await dismissBottomSheet();
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.wrapper}>
|
||||
<FavoriteBox
|
||||
channelId={channelId}
|
||||
showSnackBar={true}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
<MutedBox
|
||||
channelId={channelId}
|
||||
showSnackBar={true}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
{channelType && DIRECT_CHANNELS.includes(channelType) &&
|
||||
<SetHeaderBox channelId={channelId}/>
|
||||
}
|
||||
{channelType && !DIRECT_CHANNELS.includes(channelType) &&
|
||||
<>
|
||||
<AddPeopleBox channelId={channelId}/>
|
||||
<View style={styles.separator}/>
|
||||
<CopyChannelLinkBox
|
||||
channelId={channelId}
|
||||
onAnimationEnd={onCopyLinkAnimationEnd}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</View>
|
||||
<InfoBox
|
||||
channelId={channelId}
|
||||
showAsLabel={true}
|
||||
/>
|
||||
<View style={{backgroundColor: changeOpacity(theme.centerChannelColor, 0.08), height: 1, marginVertical: 8}}/>
|
||||
<LeaveChannelLabel channelId={channelId}/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelQuickAction;
|
||||
|
|
@ -7,7 +7,7 @@ import {StyleSheet, View} from 'react-native';
|
|||
import Animated, {FadeInDown, FadeOutUp} from 'react-native-reanimated';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import OptionBox from '@components/option_box';
|
||||
import OptionBox, {OPTIONS_HEIGHT} from '@components/option_box';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {showModal} from '@screens/navigation';
|
||||
|
|
@ -18,8 +18,6 @@ type Props = {
|
|||
close: () => Promise<void>;
|
||||
}
|
||||
|
||||
const OPTIONS_HEIGHT = 60;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginTop: 20,
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const CategoryBody = ({sortedChannels, category, hiddenChannelIds, limit, onChan
|
|||
}
|
||||
|
||||
if (category.type === DMS_CATEGORY && limit > 0) {
|
||||
return filteredChannels.slice(0, limit - 1);
|
||||
return filteredChannels.slice(0, limit);
|
||||
}
|
||||
return filteredChannels;
|
||||
}, [category.type, limit, hiddenChannelIds, sortedChannels]);
|
||||
|
|
|
|||
|
|
@ -72,11 +72,12 @@ exports[`components/channel_list/categories/header should match snapshot 1`] = `
|
|||
"fontSize": 12,
|
||||
"fontWeight": "600",
|
||||
"lineHeight": 16,
|
||||
"textTransform": "uppercase",
|
||||
}
|
||||
}
|
||||
testID="category_header.custom.display_name"
|
||||
>
|
||||
TEST CATEGORY
|
||||
Test Category
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Text, TouchableOpacity, View} from 'react-native';
|
||||
import Animated, {Easing, useAnimatedStyle, useDerivedValue, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
|
||||
import {toggleCollapseCategory} from '@actions/local/category';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {CHANNELS_CATEGORY, FAVORITES_CATEGORY, DMS_CATEGORY} from '@constants/categories';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
|
@ -25,6 +27,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
},
|
||||
heading: {
|
||||
color: changeOpacity(theme.sidebarText, 0.64),
|
||||
textTransform: 'uppercase',
|
||||
...typography('Heading', 75),
|
||||
},
|
||||
chevron: {
|
||||
|
|
@ -47,6 +50,7 @@ type Props = {
|
|||
const AnimatedCompassIcon = Animated.createAnimatedComponent(CompassIcon);
|
||||
|
||||
const CategoryHeader = ({category, hasChannels}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const serverUrl = useServerUrl();
|
||||
|
|
@ -73,10 +77,23 @@ const CategoryHeader = ({category, hasChannels}: Props) => {
|
|||
}, [category.collapsed]);
|
||||
|
||||
// Hide favs if empty
|
||||
if (!hasChannels && category.type === 'favorites') {
|
||||
if (!hasChannels && category.type === FAVORITES_CATEGORY) {
|
||||
return (null);
|
||||
}
|
||||
|
||||
let displayName = category.displayName;
|
||||
switch (category.type) {
|
||||
case FAVORITES_CATEGORY:
|
||||
displayName = formatMessage({id: 'channel_list.favorites_category', defaultMessage: 'Favorites'});
|
||||
break;
|
||||
case CHANNELS_CATEGORY:
|
||||
displayName = formatMessage({id: 'channel_list.channels_category', defaultMessage: 'Channels'});
|
||||
break;
|
||||
case DMS_CATEGORY:
|
||||
displayName = formatMessage({id: 'channel_list.dms_category', defaultMessage: 'Direct messages'});
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={toggleCollapse}
|
||||
|
|
@ -92,7 +109,7 @@ const CategoryHeader = ({category, hasChannels}: Props) => {
|
|||
style={styles.heading}
|
||||
testID={`category_header.${category.type}.display_name`}
|
||||
>
|
||||
{category.displayName.toUpperCase()}
|
||||
{displayName}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {Events, Screens} from '@constants';
|
|||
import {useTheme} from '@context/theme';
|
||||
import {findChannels} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {alertChannelRemove, alertTeamRemove} from '@utils/navigation';
|
||||
import {alertChannelArchived, alertChannelRemove, alertTeamRemove} from '@utils/navigation';
|
||||
import {notificationError} from '@utils/notification';
|
||||
|
||||
import Account from './account';
|
||||
|
|
@ -53,22 +53,22 @@ export default function HomeScreen(props: HomeProps) {
|
|||
}, [intl.locale]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = DeviceEventEmitter.addListener(Events.LEAVE_TEAM, (displayName: string) => {
|
||||
const leaveTeamListener = DeviceEventEmitter.addListener(Events.LEAVE_TEAM, (displayName: string) => {
|
||||
alertTeamRemove(displayName, intl);
|
||||
});
|
||||
|
||||
return () => {
|
||||
listener.remove();
|
||||
};
|
||||
}, [intl.locale]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = DeviceEventEmitter.addListener(Events.LEAVE_CHANNEL, (displayName: string) => {
|
||||
const leaveChannelListener = DeviceEventEmitter.addListener(Events.LEAVE_CHANNEL, (displayName: string) => {
|
||||
alertChannelRemove(displayName, intl);
|
||||
});
|
||||
|
||||
const archivedChannelListener = DeviceEventEmitter.addListener(Events.CHANNEL_ARCHIVED, (displayName: string) => {
|
||||
alertChannelArchived(displayName, intl);
|
||||
});
|
||||
|
||||
return () => {
|
||||
listener.remove();
|
||||
leaveTeamListener.remove();
|
||||
leaveChannelListener.remove();
|
||||
archivedChannelListener.remove();
|
||||
};
|
||||
}, [intl.locale]);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import React, {useEffect, useMemo, useRef, useState} from 'react';
|
|||
import {useIntl} from 'react-intl';
|
||||
import {DeviceEventEmitter, Text, TouchableOpacity, useWindowDimensions, ViewStyle} from 'react-native';
|
||||
import {Gesture, GestureDetector, GestureHandlerRootView} from 'react-native-gesture-handler';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
import {ComponentEvent, Navigation} from 'react-native-navigation';
|
||||
import Animated, {
|
||||
AnimatedStyleProp,
|
||||
Extrapolation,
|
||||
|
|
@ -205,7 +205,12 @@ const SnackBar = ({barType, componentId, onAction, sourceScreen}: SnackBarProps)
|
|||
|
||||
// This effect checks if we are navigating away and if so, it dismisses the snack bar
|
||||
useEffect(() => {
|
||||
const onHideSnackBar = () => animateHiding(true);
|
||||
const onHideSnackBar = (event?: ComponentEvent) => {
|
||||
if (componentId !== event?.componentId) {
|
||||
animateHiding(true);
|
||||
}
|
||||
};
|
||||
|
||||
const screenWillAppear = Navigation.events().registerComponentWillAppearListener(onHideSnackBar);
|
||||
const screenDidDisappear = Navigation.events().registerComponentDidDisappearListener(onHideSnackBar);
|
||||
const tabPress = DeviceEventEmitter.addListener('tabPress', onHideSnackBar);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class EphemeralStore {
|
|||
// and make sure we only handle one.
|
||||
private addingTeam = new Set<string>();
|
||||
private joiningChannels = new Set<string>();
|
||||
private leavingChannels = new Set<string>();
|
||||
|
||||
addNavigationComponentId = (componentId: string) => {
|
||||
this.addToNavigationComponentIdStack(componentId);
|
||||
|
|
@ -133,6 +134,19 @@ class EphemeralStore {
|
|||
}
|
||||
};
|
||||
|
||||
// Ephemeral control when leaving a channel locally
|
||||
addLeavingChannel = (channelId: string) => {
|
||||
this.leavingChannels.add(channelId);
|
||||
};
|
||||
|
||||
isLeavingChannel = (channelId: string) => {
|
||||
return this.leavingChannels.has(channelId);
|
||||
};
|
||||
|
||||
removeLeavingChannel = (channelId: string) => {
|
||||
this.leavingChannels.delete(channelId);
|
||||
};
|
||||
|
||||
// Ephemeral control when joining a channel locally
|
||||
addJoiningChannel = (channelId: string) => {
|
||||
this.joiningChannels.add(channelId);
|
||||
|
|
@ -142,10 +156,11 @@ class EphemeralStore {
|
|||
return this.joiningChannels.has(channelId);
|
||||
};
|
||||
|
||||
removeJoiningChanel = (channelId: string) => {
|
||||
removeJoiningChannel = (channelId: string) => {
|
||||
this.joiningChannels.delete(channelId);
|
||||
};
|
||||
|
||||
// Ephemeral control when adding a team locally
|
||||
startAddingToTeam = (teamId: string) => {
|
||||
this.addingTeam.add(teamId);
|
||||
};
|
||||
|
|
@ -158,6 +173,7 @@ class EphemeralStore {
|
|||
return this.addingTeam.has(teamId);
|
||||
};
|
||||
|
||||
// Ephemeral for push proxy state
|
||||
setPushProxyVerificationState = (serverUrl: string, state: string) => {
|
||||
this.pushProxyVerification[serverUrl] = state;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export function makeCategoryId(name: string, userId: string, teamId: string) {
|
||||
return `${name}_${userId}_${teamId}`;
|
||||
}
|
||||
|
||||
export function makeCategoryChannelId(teamId: string, channelId: string) {
|
||||
return `${teamId}_${channelId}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export function mergeNavigationOptions(componentId: string, options: Options) {
|
|||
Navigation.mergeOptions(componentId, options);
|
||||
}
|
||||
|
||||
export async function alertTeamRemove(displayName: string, intl: IntlShape) {
|
||||
export function alertTeamRemove(displayName: string, intl: IntlShape) {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'alert.removed_from_team.title',
|
||||
|
|
@ -30,7 +30,7 @@ export async function alertTeamRemove(displayName: string, intl: IntlShape) {
|
|||
);
|
||||
}
|
||||
|
||||
export async function alertChannelRemove(displayName: string, intl: IntlShape) {
|
||||
export function alertChannelRemove(displayName: string, intl: IntlShape) {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'alert.removed_from_channel.title',
|
||||
|
|
@ -46,3 +46,20 @@ export async function alertChannelRemove(displayName: string, intl: IntlShape) {
|
|||
}],
|
||||
);
|
||||
}
|
||||
|
||||
export function alertChannelArchived(displayName: string, intl: IntlShape) {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'alert.channel_deleted.title',
|
||||
defaultMessage: 'Archived channel',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'alert.channel_deleted.description',
|
||||
defaultMessage: 'The channel {displayName} has been archived.',
|
||||
}, {displayName}),
|
||||
[{
|
||||
style: 'cancel',
|
||||
text: intl.formatMessage({id: 'mobile.oauth.something_wrong.okButton', defaultMessage: 'OK'}),
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,16 @@ export const showSnackBar = (passProps: ShowSnackBarArgs) => {
|
|||
showOverlay(screen, passProps);
|
||||
};
|
||||
|
||||
export const showMuteChannelSnackbar = (onAction: () => void) => {
|
||||
export const showMuteChannelSnackbar = (muted: boolean, onAction: () => void) => {
|
||||
return showSnackBar({
|
||||
onAction,
|
||||
barType: SNACK_BAR_TYPE.MUTE_CHANNEL,
|
||||
barType: muted ? SNACK_BAR_TYPE.MUTE_CHANNEL : SNACK_BAR_TYPE.UNMUTE_CHANNEL,
|
||||
});
|
||||
};
|
||||
|
||||
export const showFavoriteChannelSnackbar = (favorited: boolean, onAction: () => void) => {
|
||||
return showSnackBar({
|
||||
onAction,
|
||||
barType: favorited ? SNACK_BAR_TYPE.FAVORITE_CHANNEL : SNACK_BAR_TYPE.UNFAVORITE_CHANNEL,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
"account.settings": "Settings",
|
||||
"account.user_status.title": "User Presence",
|
||||
"account.your_profile": "Your Profile",
|
||||
"alert.channel_deleted.description": "The channel {displayName} has been archived.",
|
||||
"alert.channel_deleted.title": "Archived channel",
|
||||
"alert.push_proxy_error.description": "Due to the configuration for this server, notifications cannot be received in the mobile app. Contact your system admin for more information.",
|
||||
"alert.push_proxy_error.title": "Notifications cannot be received from this server",
|
||||
"alert.push_proxy_unknown.description": "This server was unable to receive push notifications for an unknown reason. This will be attempted again next time you connect.",
|
||||
|
|
@ -95,8 +97,28 @@
|
|||
"camera_type.title": "Choose an action",
|
||||
"camera_type.video.option": "Record Video",
|
||||
"center_panel.archived.closeChannel": "Close Channel",
|
||||
"channel": "{count, plural, one {# member} other {# members}}",
|
||||
"channel_header.directchannel.you": "{displayName} (you)",
|
||||
"channel_header.info": "View info",
|
||||
"channel_header.member_count": "{count, plural, one {# member} other {# members}}",
|
||||
"channel_info.close": "Close",
|
||||
"channel_info.close_dm": "Close direct message",
|
||||
"channel_info.close_dm_channel": "Are you sure you want to close this direct message? This will remove it from your home screen, but you can always open it again.",
|
||||
"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.copied": "Copied",
|
||||
"channel_info.copy_link": "Copy Link",
|
||||
"channel_info.edit_header": "Edit Header",
|
||||
"channel_info.favorite": "Favorite",
|
||||
"channel_info.favorited": "Favorited",
|
||||
"channel_info.leave": "Leave",
|
||||
"channel_info.leave_channel": "Leave channel",
|
||||
"channel_info.leave_private_channel": "Are you sure you want to leave the private channel {displayName}? You cannot rejoin the channel unless you're invited again.",
|
||||
"channel_info.leave_public_channel": "Are you sure you want to leave the public channel {displayName}? You can always rejoin.",
|
||||
"channel_info.muted": "Mute",
|
||||
"channel_info.set_header": "Set Header",
|
||||
"channel_list.channels_category": "Channels",
|
||||
"channel_list.dms_category": "Direct messages",
|
||||
"channel_list.favorites_category": "Favorites",
|
||||
"channel_list.find_channels": "Find channels...",
|
||||
"channel_loader.someone": "Someone",
|
||||
"channel_modal.descriptionHelp": "Describe how this channel should be used.",
|
||||
|
|
@ -110,7 +132,6 @@
|
|||
"channel_modal.optional": "(optional)",
|
||||
"channel_modal.purpose": "Purpose",
|
||||
"channel_modal.purposeEx": "A channel to file bugs and improvements",
|
||||
"channel.details": "View details",
|
||||
"combined_system_message.added_to_channel.many_expanded": "{users} and {lastUser} were **added to the channel** by {actor}.",
|
||||
"combined_system_message.added_to_channel.one": "{firstUser} **added to the channel** by {actor}.",
|
||||
"combined_system_message.added_to_channel.one_you": "You were **added to the channel** by {actor}.",
|
||||
|
|
@ -241,14 +262,12 @@
|
|||
"home.header.plus_menu": "Options",
|
||||
"interactive_dialog.submit": "Submit",
|
||||
"intro.add_people": "Add People",
|
||||
"intro.channel_details": "Details",
|
||||
"intro.channel_info": "Info",
|
||||
"intro.created_by": "created by {creator} on {date}.",
|
||||
"intro.direct_message": "This is the start of your conversation with {teammate}. Messages and files shared here are not shown to anyone else.",
|
||||
"intro.favorite": "Favorite",
|
||||
"intro.group_message": "This is the start of your conversation with this group. Messages and files shared here are not shown to anyone else outside of the group.",
|
||||
"intro.private_channel": "Private Channel",
|
||||
"intro.public_channel": "Public Channel",
|
||||
"intro.set_header": "Set Header",
|
||||
"intro.townsquare": "Welcome to {name}. Everyone automatically becomes a member of this channel when they join the team.",
|
||||
"intro.welcome": "Welcome to {displayName} channel.",
|
||||
"intro.welcome.private": "Only invited members can see messages posted in this private channel.",
|
||||
|
|
@ -546,8 +565,8 @@
|
|||
"screen.mentions.subtitle": "Messages you've been mentioned in",
|
||||
"screen.mentions.title": "Recent Mentions",
|
||||
"screen.search.placeholder": "Search messages & files",
|
||||
"screens.channel_details": "Channel Details",
|
||||
"screens.channel_edit_header": "Edit Channel Header",
|
||||
"screens.channel_info": "Channel Info",
|
||||
"search_bar.search": "Search",
|
||||
"select_team.description": "You are not yet a member of any teams. Select one below to get started.",
|
||||
"select_team.no_team.description": "To join a team, ask a team admin for an invite, or create your own team. You may also want to check your email inbox for an invitation.",
|
||||
|
|
@ -567,10 +586,13 @@
|
|||
"servers.login": "Log in",
|
||||
"servers.logout": "Log out",
|
||||
"servers.remove": "Remove",
|
||||
"snack.bar.favorited.channel": "This channel was favorited",
|
||||
"snack.bar.link.copied": "Link copied to clipboard",
|
||||
"snack.bar.message.copied": "Text copied to clipboard",
|
||||
"snack.bar.mute.channel": "This channel was muted",
|
||||
"snack.bar.undo": "Undo",
|
||||
"snack.bar.unfavorite.channel": "This channel was unfavorited",
|
||||
"snack.bar.unmute.channel": "This channel was unmuted",
|
||||
"status_dropdown.set_away": "Away",
|
||||
"status_dropdown.set_dnd": "Do Not Disturb",
|
||||
"status_dropdown.set_offline": "Offline",
|
||||
|
|
|
|||
3
types/database/models/servers/category.d.ts
vendored
3
types/database/models/servers/category.d.ts
vendored
|
|
@ -59,4 +59,7 @@ export default class CategoryModel extends Model {
|
|||
|
||||
/** hasChannels : Whether the category has any channels */
|
||||
@lazy hasChannels: Observable<boolean>;
|
||||
|
||||
/** toCategoryWithChannels returns a map of the Category with an array of ordered channel ids */
|
||||
toCategoryWithChannels(): Promise<CategoryWithChannels>;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue