Merge branch 'gekidou' into MM-35065-add-onboarding-screens
This commit is contained in:
commit
31f0a27a5a
188 changed files with 8453 additions and 12730 deletions
|
|
@ -111,7 +111,9 @@ commands:
|
|||
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
|
||||
- run:
|
||||
name: Getting JavaScript dependencies
|
||||
command: NODE_ENV=development npm ci --ignore-scripts
|
||||
command: |
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
node node_modules/\@sentry/cli/scripts/install.js
|
||||
- save_cache:
|
||||
name: Save npm cache
|
||||
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ android {
|
|||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 432
|
||||
versionCode 438
|
||||
versionName "2.0.0"
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
getMyChannel, getChannelById, queryUsersOnChannel, queryUserChannelsByTypes,
|
||||
} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {prepareCommonSystemValues, PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId} from '@queries/servers/system';
|
||||
import {prepareCommonSystemValues, PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId, getConfig, getLicense} from '@queries/servers/system';
|
||||
import {addChannelToTeamHistory, addTeamToTeamHistory, getTeamById, removeChannelFromTeamHistory} from '@queries/servers/team';
|
||||
import {getCurrentUser, queryUsersById} from '@queries/servers/user';
|
||||
import {dismissAllModalsAndPopToRoot, dismissAllModalsAndPopToScreen} from '@screens/navigation';
|
||||
|
|
@ -365,9 +365,10 @@ export async function updateChannelsDisplayName(serverUrl: string, channels: Cha
|
|||
return {};
|
||||
}
|
||||
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const displaySettings = getTeammateNameDisplaySetting(preferences, config, license);
|
||||
const displaySettings = getTeammateNameDisplaySetting(preferences, config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
const models: Model[] = [];
|
||||
for await (const channel of channels) {
|
||||
let newDisplayName = '';
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import deepEqual from 'deep-equal';
|
|||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
import {getCommonSystemValues} from '@queries/servers/system';
|
||||
import {getConfig, getLicense} from '@queries/servers/system';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
export async function storeConfigAndLicense(serverUrl: string, config: ClientConfig, license: ClientLicense) {
|
||||
|
|
@ -14,17 +14,11 @@ export async function storeConfigAndLicense(serverUrl: string, config: ClientCon
|
|||
// If we have credentials for this server then update the values in the database
|
||||
const credentials = await getServerCredentials(serverUrl);
|
||||
if (credentials) {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const current = await getCommonSystemValues(operator.database);
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentLicense = await getLicense(database);
|
||||
const systems: IdValue[] = [];
|
||||
if (!deepEqual(config, current.config)) {
|
||||
systems.push({
|
||||
id: SYSTEM_IDENTIFIERS.CONFIG,
|
||||
value: JSON.stringify(config),
|
||||
});
|
||||
}
|
||||
|
||||
if (!deepEqual(license, current.license)) {
|
||||
if (!deepEqual(license, currentLicense)) {
|
||||
systems.push({
|
||||
id: SYSTEM_IDENTIFIERS.LICENSE,
|
||||
value: JSON.stringify(license),
|
||||
|
|
@ -34,8 +28,43 @@ export async function storeConfigAndLicense(serverUrl: string, config: ClientCon
|
|||
if (systems.length) {
|
||||
await operator.handleSystem({systems, prepareRecordsOnly: false});
|
||||
}
|
||||
|
||||
await storeConfig(serverUrl, config);
|
||||
}
|
||||
} catch (error) {
|
||||
logError('An error occurred while saving config & license', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function storeConfig(serverUrl: string, config: ClientConfig | undefined, prepareRecordsOnly = false) {
|
||||
if (!config) {
|
||||
return [];
|
||||
}
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentConfig = await getConfig(database);
|
||||
const configsToUpdate: IdValue[] = [];
|
||||
const configsToDelete: IdValue[] = [];
|
||||
|
||||
let k: keyof ClientConfig;
|
||||
for (k in config) {
|
||||
if (currentConfig?.[k] !== config[k]) {
|
||||
configsToUpdate.push({
|
||||
id: k,
|
||||
value: config[k],
|
||||
});
|
||||
}
|
||||
}
|
||||
for (k in currentConfig) {
|
||||
if (config[k] === undefined) {
|
||||
configsToDelete.push({
|
||||
id: k,
|
||||
value: currentConfig[k],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (configsToDelete.length || configsToUpdate.length) {
|
||||
return operator.handleConfigs({configs: configsToUpdate, configsToDelete, prepareRecordsOnly});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import DatabaseManager from '@database/manager';
|
|||
import {getTranslations, t} from '@i18n';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {getPostById} from '@queries/servers/post';
|
||||
import {getCommonSystemValues, getCurrentTeamId, getCurrentUserId, prepareCommonSystemValues, PrepareCommonSystemValuesArgs, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getCurrentTeamId, getCurrentUserId, prepareCommonSystemValues, PrepareCommonSystemValuesArgs, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {addChannelToTeamHistory, addTeamToTeamHistory} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled, getThreadById, prepareThreadsFromReceivedPosts, queryThreadsInTeam} from '@queries/servers/thread';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
|
|
@ -74,12 +74,12 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo
|
|||
throw new Error('Channel not found');
|
||||
}
|
||||
|
||||
const system = await getCommonSystemValues(database);
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const isTabletDevice = await isTablet();
|
||||
const teamId = channel.teamId || system.currentTeamId;
|
||||
const teamId = channel.teamId || currentTeamId;
|
||||
|
||||
let switchingTeams = false;
|
||||
if (system.currentTeamId !== teamId) {
|
||||
if (currentTeamId !== teamId) {
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
switchingTeams = true;
|
||||
modelPromises.push(addTeamToTeamHistory(operator, teamId, true));
|
||||
|
|
|
|||
|
|
@ -523,7 +523,7 @@ export async function fetchDirectChannelsInfo(serverUrl: string, directChannels:
|
|||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).fetch();
|
||||
const config = await getConfig(database);
|
||||
const license = await getLicense(database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences, config, license);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences, config?.LockTeammateNameDisplay, config?.TeammateNameDisplay, license);
|
||||
const currentUser = await getCurrentUser(database);
|
||||
const channels = directChannels.map((d) => d.toApi());
|
||||
return fetchMissingDirectChannelsInfo(serverUrl, channels, currentUser?.locale, teammateDisplayNameSetting, currentUser?.id);
|
||||
|
|
@ -743,8 +743,9 @@ export async function createDirectChannel(serverUrl: string, userId: string, dis
|
|||
created.display_name = displayName;
|
||||
} else {
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const system = await getCommonSystemValues(database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], system.config, system.license);
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
const {directChannels, users} = await fetchMissingDirectChannelsInfo(serverUrl, [created], currentUser.locale, teammateDisplayNameSetting, currentUser.id, true);
|
||||
created.display_name = directChannels?.[0].display_name || created.display_name;
|
||||
if (users?.length) {
|
||||
|
|
@ -860,19 +861,16 @@ export async function fetchArchivedChannels(serverUrl: string, teamId: string, p
|
|||
}
|
||||
|
||||
export async function createGroupChannel(serverUrl: string, userIds: string[]) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const currentUser = await getCurrentUser(operator.database);
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentUser = await getCurrentUser(database);
|
||||
if (!currentUser) {
|
||||
return {error: 'Cannot get the current user'};
|
||||
}
|
||||
|
|
@ -886,9 +884,10 @@ export async function createGroupChannel(serverUrl: string, userIds: string[]) {
|
|||
return {data: created};
|
||||
}
|
||||
|
||||
const preferences = await queryPreferencesByCategoryAndName(operator.database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const system = await getCommonSystemValues(operator.database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], system.config, system.license);
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
const {directChannels, users} = await fetchMissingDirectChannelsInfo(serverUrl, [created], currentUser.locale, teammateDisplayNameSetting, currentUser.id, true);
|
||||
|
||||
const member = {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {prepareCommonSystemValues, getCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId, getCurrentChannelId} from '@queries/servers/system';
|
||||
import {prepareCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId, getCurrentChannelId, getConfig, getLicense} from '@queries/servers/system';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {deleteV1Data} from '@utils/file';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
|
|
@ -65,7 +65,8 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
|
|||
logInfo('ENTRY MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = meData?.user || (await getCurrentUser(database))!;
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
const config = await getConfig(database);
|
||||
const license = await getLicense(database);
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchToChannel ? initialChannelId : undefined);
|
||||
|
||||
if (!since) {
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, ini
|
|||
|
||||
const confReq = await fetchConfigAndLicense(serverUrl);
|
||||
const prefData = await fetchMyPreferences(serverUrl, fetchOnly);
|
||||
const isCRTEnabled = Boolean(prefData.preferences && processIsCRTEnabled(prefData.preferences, confReq.config));
|
||||
const isCRTEnabled = Boolean(prefData.preferences && processIsCRTEnabled(prefData.preferences, confReq.config?.CollapsedThreads, confReq.config?.FeatureFlagCollapsedThreads));
|
||||
if (prefData.preferences) {
|
||||
const crtToggled = await getHasCRTChanged(database, prefData.preferences);
|
||||
if (crtToggled) {
|
||||
|
|
@ -306,7 +306,7 @@ export async function entryInitialChannelId(database: Database, requestedChannel
|
|||
|
||||
export async function restDeferredAppEntryActions(
|
||||
serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined,
|
||||
config: ClientConfig, license: ClientLicense, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
|
||||
config: ClientConfig, license: ClientLicense | undefined, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
|
||||
initialTeamId?: string, initialChannelId?: string) {
|
||||
// defer sidebar DM & GM profiles
|
||||
let channelsToFetchProfiles: Set<Channel>|undefined;
|
||||
|
|
@ -325,7 +325,7 @@ export async function restDeferredAppEntryActions(
|
|||
fetchTeamsChannelsAndUnreadPosts(serverUrl, since, teamData.teams, teamData.memberships, initialTeamId);
|
||||
}
|
||||
|
||||
if (preferences && processIsCRTEnabled(preferences, config)) {
|
||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads)) {
|
||||
if (initialTeamId) {
|
||||
await fetchNewThreads(serverUrl, initialTeamId, false);
|
||||
}
|
||||
|
|
@ -348,7 +348,7 @@ export async function restDeferredAppEntryActions(
|
|||
|
||||
setTimeout(async () => {
|
||||
if (channelsToFetchProfiles?.size) {
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config, license);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), currentUserLocale, teammateDisplayNameSetting, currentUserId);
|
||||
}
|
||||
}, FETCH_MISSING_DM_TIMEOUT);
|
||||
|
|
@ -454,7 +454,7 @@ const restSyncAllChannelMembers = async (serverUrl: string) => {
|
|||
for (const myTeam of myTeams) {
|
||||
fetchMyChannelsForTeam(serverUrl, myTeam.id, false, 0, false, excludeDirect);
|
||||
excludeDirect = true;
|
||||
if (preferences && processIsCRTEnabled(preferences, config)) {
|
||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads)) {
|
||||
fetchNewThreads(serverUrl, myTeam.id, false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export async function deferredAppEntryGraphQLActions(
|
|||
}
|
||||
}, FETCH_UNREADS_TIMEOUT);
|
||||
|
||||
if (preferences && processIsCRTEnabled(preferences, config)) {
|
||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads)) {
|
||||
if (initialTeamId) {
|
||||
await fetchNewThreads(serverUrl, initialTeamId, false);
|
||||
}
|
||||
|
|
@ -256,7 +256,7 @@ export const entry = async (serverUrl: string, teamId?: string, channelId?: stri
|
|||
|
||||
export async function deferredAppEntryActions(
|
||||
serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined,
|
||||
config: ClientConfig, license: ClientLicense, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
|
||||
config: ClientConfig, license: ClientLicense | undefined, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
|
||||
initialTeamId?: string, initialChannelId?: string) {
|
||||
let result;
|
||||
if (config?.FeatureFlagGraphQL === 'true') {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {getDefaultThemeByAppearance} from '@context/theme';
|
|||
import DatabaseManager from '@database/manager';
|
||||
import {getMyChannel} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {getCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getConfig, getCurrentTeamId, getLicense, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getMyTeamById} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
|
|
@ -136,7 +136,8 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
|
|||
await operator.batchRecords(models);
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(operator.database))!;
|
||||
const {config, license} = await getCommonSystemValues(operator.database);
|
||||
const config = await getConfig(database);
|
||||
const license = await getLicense(database);
|
||||
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, selectedChannelId);
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {fetchMyTeam} from '@actions/remote/team';
|
|||
import {fetchAndSwitchToThread} from '@actions/remote/thread';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getMyChannel, getChannelById} from '@queries/servers/channel';
|
||||
import {getCommonSystemValues, getWebSocketLastDisconnected} from '@queries/servers/system';
|
||||
import {getCurrentTeamId, getWebSocketLastDisconnected} from '@queries/servers/system';
|
||||
import {getMyTeamById} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||
import {emitNotificationError} from '@utils/notification';
|
||||
|
|
@ -30,14 +30,14 @@ const fetchNotificationData = async (serverUrl: string, notification: Notificati
|
|||
}
|
||||
|
||||
const {database} = operator;
|
||||
const system = await getCommonSystemValues(database);
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
let teamId = notification.payload?.team_id;
|
||||
let isDirectChannel = false;
|
||||
|
||||
if (!teamId) {
|
||||
// If the notification payload does not have a teamId we assume is a DM/GM
|
||||
isDirectChannel = true;
|
||||
teamId = system.currentTeamId;
|
||||
teamId = currentTeamId;
|
||||
}
|
||||
|
||||
// To make the switch faster we determine if we already have the team & channel
|
||||
|
|
@ -129,13 +129,13 @@ export const openNotification = async (serverUrl: string, notification: Notifica
|
|||
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
const isThreadNotification = isCRTEnabled && Boolean(rootId);
|
||||
|
||||
const system = await getCommonSystemValues(database);
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const currentServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
let teamId = notification.payload?.team_id;
|
||||
|
||||
if (!teamId) {
|
||||
// If the notification payload does not have a teamId we assume is a DM/GM
|
||||
teamId = system.currentTeamId;
|
||||
teamId = currentTeamId;
|
||||
}
|
||||
|
||||
if (currentServerUrl !== serverUrl) {
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ export const saveFavoriteChannel = async (serverUrl: string, channelId: string,
|
|||
}
|
||||
|
||||
try {
|
||||
// Todo: @shaz I think you'll need to add the category handler here so that the channel is added/removed from the favorites category
|
||||
const userId = await getCurrentUserId(operator.database);
|
||||
const favPref: PreferenceType = {
|
||||
category: CATEGORY_FAVORITE_CHANNEL,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {storeConfig} from '@actions/local/systems';
|
||||
import {Preferences} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPreferenceValue, getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
|
|
@ -9,7 +10,7 @@ import NetworkManager from '@managers/network_manager';
|
|||
import {prepareCategories, prepareCategoryChannels} from '@queries/servers/categories';
|
||||
import {prepareMyChannelsForTeam} from '@queries/servers/channel';
|
||||
import {prepareMyPreferences, queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {prepareCommonSystemValues, getCommonSystemValues} from '@queries/servers/system';
|
||||
import {prepareCommonSystemValues, getConfig, getLicense} from '@queries/servers/system';
|
||||
import {prepareMyTeams} from '@queries/servers/team';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {isDMorGM, selectDefaultChannelForTeam} from '@utils/channel';
|
||||
|
|
@ -101,14 +102,15 @@ export async function retryInitialTeamAndChannel(serverUrl: string) {
|
|||
|
||||
const models: Model[] = (await Promise.all([
|
||||
prepareMyPreferences(operator, prefData.preferences!),
|
||||
storeConfig(serverUrl, clData.config, true),
|
||||
...prepareMyTeams(operator, teamData.teams!, teamData.memberships!),
|
||||
...await prepareMyChannelsForTeam(operator, initialTeam.id, chData!.channels!, chData!.memberships!),
|
||||
prepareCategories(operator, chData!.categories!),
|
||||
prepareCategoryChannels(operator, chData!.categories!),
|
||||
|
||||
prepareCommonSystemValues(
|
||||
operator,
|
||||
{
|
||||
config: clData.config!,
|
||||
license: clData.license!,
|
||||
currentTeamId: initialTeam?.id,
|
||||
currentChannelId: initialChannel?.id,
|
||||
|
|
@ -121,7 +123,7 @@ export async function retryInitialTeamAndChannel(serverUrl: string) {
|
|||
const directChannels = chData!.channels!.filter(isDMorGM);
|
||||
const channelsToFetchProfiles = new Set<Channel>(directChannels);
|
||||
if (channelsToFetchProfiles.size) {
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(prefData.preferences || [], clData.config, clData.license);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(prefData.preferences || [], clData.config?.LockTeammateNameDisplay, clData.config?.TeammateNameDisplay, clData.license);
|
||||
fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), user.locale, teammateDisplayNameSetting, user.id);
|
||||
}
|
||||
|
||||
|
|
@ -163,7 +165,8 @@ export async function retryInitialChannel(serverUrl: string, teamId: string) {
|
|||
user_id: p.userId,
|
||||
value: p.value,
|
||||
}));
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
|
||||
// fetch channels / channel membership for initial team
|
||||
const chData = await fetchMyChannelsForTeam(serverUrl, teamId, false, 0, true);
|
||||
|
|
@ -200,7 +203,7 @@ export async function retryInitialChannel(serverUrl: string, teamId: string) {
|
|||
const directChannels = chData!.channels!.filter(isDMorGM);
|
||||
const channelsToFetchProfiles = new Set<Channel>(directChannels);
|
||||
if (channelsToFetchProfiles.size) {
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config, license);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), user.locale, teammateDisplayNameSetting, user.id);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import NetworkManager from '@managers/network_manager';
|
|||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {getDeviceToken} from '@queries/app/global';
|
||||
import {queryServerName} from '@queries/app/servers';
|
||||
import {getCurrentUserId, getCommonSystemValues, getExpiredSession} from '@queries/servers/system';
|
||||
import {getCurrentUserId, getExpiredSession, getConfig, getLicense} from '@queries/servers/system';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {logWarning, logError} from '@utils/log';
|
||||
|
|
@ -35,9 +35,10 @@ export const completeLogin = async (serverUrl: string) => {
|
|||
}
|
||||
|
||||
const {database} = operator;
|
||||
const {config, license}: { config: Partial<ClientConfig>; license: Partial<ClientLicense> } = await getCommonSystemValues(database);
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
|
||||
if (!Object.keys(config)?.length || !Object.keys(license)?.length) {
|
||||
if (!Object.keys(config)?.length || !license || !Object.keys(license)?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import PushNotifications from '@init/push_notifications';
|
|||
import AppsManager from '@managers/apps_manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getPostById} from '@queries/servers/post';
|
||||
import {getCommonSystemValues, getCurrentChannelId, getCurrentTeamId} from '@queries/servers/system';
|
||||
import {getConfigValue, getCurrentChannelId, getCurrentTeamId} from '@queries/servers/system';
|
||||
import {getIsCRTEnabled, getNewestThreadInTeam, getThreadById} from '@queries/servers/thread';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
|
||||
|
|
@ -104,9 +104,8 @@ export const fetchThreads = async (
|
|||
}
|
||||
|
||||
try {
|
||||
const {config} = await getCommonSystemValues(database);
|
||||
|
||||
const data = await client.getThreads('me', teamId, before, after, perPage, deleted, unread, since, false, config.Version);
|
||||
const version = await getConfigValue(database, 'Version');
|
||||
const data = await client.getThreads('me', teamId, before, after, perPage, deleted, unread, since, false, version);
|
||||
|
||||
const {threads} = data;
|
||||
|
||||
|
|
@ -323,7 +322,7 @@ async function fetchBatchThreads(
|
|||
return {error: 'currentUser not found'};
|
||||
}
|
||||
|
||||
const {config} = await getCommonSystemValues(operator.database);
|
||||
const version = await getConfigValue(operator.database, 'Version');
|
||||
const data: Thread[] = [];
|
||||
|
||||
const fetchThreadsFunc = async (opts: FetchThreadsOptions) => {
|
||||
|
|
@ -331,7 +330,7 @@ async function fetchBatchThreads(
|
|||
const {before, after, perPage = General.CRT_CHUNK_SIZE, deleted, unread, since} = opts;
|
||||
|
||||
page += 1;
|
||||
const {threads} = await client.getThreads(currentUser.id, teamId, before, after, perPage, deleted, unread, since, false, config.Version);
|
||||
const {threads} = await client.getThreads(currentUser.id, teamId, before, after, perPage, deleted, unread, since, false, version);
|
||||
if (threads.length) {
|
||||
// Mark all fetched threads as following
|
||||
for (const thread of threads) {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fetchGroupsForChannel, fetchGroupsForMember, fetchGroupsForTeam} from '@actions/remote/groups';
|
||||
import {deleteGroupChannelById, deleteGroupMembershipById, deleteGroupTeamById} from '@app/queries/servers/group';
|
||||
import {generateGroupAssociationId} from '@app/utils/groups';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {deleteGroupChannelById, deleteGroupMembershipById, deleteGroupTeamById} from '@queries/servers/group';
|
||||
import {generateGroupAssociationId} from '@utils/groups';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
type WebsocketGroupMessage = WebSocketMessage<{
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ import AppsManager from '@managers/apps_manager';
|
|||
import {getActiveServerUrl, queryActiveServer} from '@queries/app/servers';
|
||||
import {getCurrentChannel} from '@queries/servers/channel';
|
||||
import {
|
||||
getCommonSystemValues,
|
||||
getConfig,
|
||||
getCurrentUserId,
|
||||
getLicense,
|
||||
getWebSocketLastDisconnected,
|
||||
resetWebSocketLastDisconnected,
|
||||
setCurrentTeamAndChannelId,
|
||||
|
|
@ -178,13 +178,15 @@ async function doReconnect(serverUrl: string) {
|
|||
logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(database))!;
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchedToChannel ? initialChannelId : undefined);
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
|
||||
if (isSupportedServerCalls(config?.Version)) {
|
||||
loadConfigAndCalls(serverUrl, currentUserId);
|
||||
}
|
||||
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchedToChannel ? initialChannelId : undefined);
|
||||
|
||||
AppsManager.refreshAppBindings(serverUrl);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -128,10 +128,7 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
|
|||
) {
|
||||
markAsViewed = true;
|
||||
markAsRead = false;
|
||||
} else if ((post.channel_id === currentChannelId)) { // TODO: THREADS && !viewingGlobalThreads) {
|
||||
// Don't mark as read if we're in global threads screen
|
||||
// the currentChannelId still refers to previously viewed channel
|
||||
|
||||
} else if ((post.channel_id === currentChannelId)) {
|
||||
const isChannelScreenMounted = NavigationStore.getNavigationComponents().includes(Screens.CHANNEL);
|
||||
|
||||
const isTabletDevice = await isTablet();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {updateDmGmDisplayName} from '@actions/local/channel';
|
||||
import {storeConfig} from '@actions/local/systems';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getConfig, getLicense} from '@queries/servers/system';
|
||||
|
|
@ -35,10 +36,8 @@ export async function handleConfigChangedEvent(serverUrl: string, msg: WebSocket
|
|||
|
||||
try {
|
||||
const config = msg.data.config;
|
||||
const systems: IdValue[] = [{id: SYSTEM_IDENTIFIERS.CONFIG, value: JSON.stringify(config)}];
|
||||
|
||||
const prevConfig = await getConfig(operator.database);
|
||||
await operator.handleSystem({systems, prepareRecordsOnly: false});
|
||||
await storeConfig(serverUrl, config);
|
||||
if (config?.LockTeammateNameDisplay && (prevConfig?.LockTeammateNameDisplay !== config.LockTeammateNameDisplay)) {
|
||||
updateDmGmDisplayName(serverUrl);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
|||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {queryChannelsByTypes, queryUserChannelsByTypes} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {getCommonSystemValues} from '@queries/servers/system';
|
||||
import {getConfig, getLicense} from '@queries/servers/system';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
|
|
@ -84,13 +84,14 @@ export async function handleUserTypingEvent(serverUrl: string, msg: WebSocketMes
|
|||
return;
|
||||
}
|
||||
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
|
||||
const {users, existingUsers} = await fetchUsersByIds(serverUrl, [msg.data.user_id]);
|
||||
const user = users?.[0] || existingUsers?.[0];
|
||||
|
||||
const namePreference = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(namePreference, config, license);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(namePreference, config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
const currentUser = await getCurrentUser(database);
|
||||
const username = displayUsername(user, currentUser?.locale, teammateDisplayNameSetting);
|
||||
const data = {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {Platform} from 'react-native';
|
|||
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getCommonSystemValues} from '@queries/servers/system';
|
||||
import {getConfig} from '@queries/servers/system';
|
||||
import {logError, logInfo, logWarning} from '@utils/log';
|
||||
|
||||
const MAX_WEBSOCKET_FAILS = 7;
|
||||
|
|
@ -75,8 +75,8 @@ export default class WebSocketClient {
|
|||
return;
|
||||
}
|
||||
|
||||
const system = await getCommonSystemValues(database);
|
||||
const connectionUrl = (system.config.WebsocketURL || this.serverUrl) + '/api/v4/websocket';
|
||||
const config = await getConfig(database);
|
||||
const connectionUrl = (config.WebsocketURL || this.serverUrl) + '/api/v4/websocket';
|
||||
|
||||
if (this.connectingCallback) {
|
||||
this.connectingCallback();
|
||||
|
|
@ -97,7 +97,7 @@ export default class WebSocketClient {
|
|||
|
||||
this.url = connectionUrl;
|
||||
|
||||
const reliableWebSockets = system.config.EnableReliableWebSockets === 'true';
|
||||
const reliableWebSockets = config.EnableReliableWebSockets === 'true';
|
||||
if (reliableWebSockets) {
|
||||
// Add connection id, and last_sequence_number to the query param.
|
||||
// We cannot also send it as part of the auth_challenge, because the session cookie is already sent with the request.
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {map, switchMap} from 'rxjs/operators';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeConfig} from '@queries/servers/system';
|
||||
import {observeConfigBooleanValue} from '@queries/servers/system';
|
||||
import {observeUser} from '@queries/servers/user';
|
||||
import {getUserCustomStatus, isCustomStatusExpired} from '@utils/user';
|
||||
|
||||
|
|
@ -21,9 +21,8 @@ type HeaderInputProps = {
|
|||
const enhanced = withObservables(
|
||||
['userId'],
|
||||
({userId, database}: WithDatabaseArgs & HeaderInputProps) => {
|
||||
const config = observeConfig(database);
|
||||
const user = observeUser(database, userId);
|
||||
const isCustomStatusEnabled = config.pipe(map((cfg) => cfg?.EnableCustomUserStatuses === 'true'));
|
||||
const isCustomStatusEnabled = observeConfigBooleanValue(database, 'EnableCustomUserStatuses');
|
||||
const customStatus = user.pipe(switchMap((u) => (u?.isBot ? of$(undefined) : of$(getUserCustomStatus(u)))));
|
||||
const customStatusExpired = user.pipe(switchMap((u) => (u?.isBot ? of$(false) : of$(isCustomStatusExpired(u)))));
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ type FileProps = {
|
|||
galleryIdentifier: string;
|
||||
index: number;
|
||||
inViewPort: boolean;
|
||||
isSingleImage: boolean;
|
||||
isSingleImage?: boolean;
|
||||
nonVisibleImagesCount: number;
|
||||
onPress: (index: number) => void;
|
||||
publicLinkEnabled: boolean;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import withObservables from '@nozbe/with-observables';
|
|||
import {combineLatest, of as of$, from as from$} from 'rxjs';
|
||||
import {map, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeConfig, observeLicense} from '@queries/servers/system';
|
||||
import {observeConfigBooleanValue, observeLicense} from '@queries/servers/system';
|
||||
import {fileExists} from '@utils/file';
|
||||
|
||||
import Files from './files';
|
||||
|
|
@ -36,14 +36,8 @@ const filesLocalPathValidation = async (files: FileModel[], authorId: string) =>
|
|||
};
|
||||
|
||||
const enhance = withObservables(['post'], ({database, post}: EnhanceProps) => {
|
||||
const config = observeConfig(database);
|
||||
const enableMobileFileDownload = config.pipe(
|
||||
switchMap((cfg) => of$(cfg?.EnableMobileFileDownload !== 'false')),
|
||||
);
|
||||
|
||||
const publicLinkEnabled = config.pipe(
|
||||
switchMap((cfg) => of$(cfg?.EnablePublicLink !== 'false')),
|
||||
);
|
||||
const enableMobileFileDownload = observeConfigBooleanValue(database, 'EnableMobileFileDownload');
|
||||
const publicLinkEnabled = observeConfigBooleanValue(database, 'EnablePublicLink');
|
||||
|
||||
const complianceDisabled = observeLicense(database).pipe(
|
||||
switchMap((lcs) => of$(lcs?.IsLicensed === 'false' || lcs?.Compliance === 'false')),
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
zIndex: 10,
|
||||
maxWidth: 315,
|
||||
},
|
||||
readOnly: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelBg, 0.16),
|
||||
},
|
||||
smallLabel: {
|
||||
fontSize: 10,
|
||||
},
|
||||
|
|
@ -191,6 +194,9 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
|
|||
|
||||
const combinedTextInputStyle = useMemo(() => {
|
||||
const res: StyleProp<TextStyle> = [styles.textInput];
|
||||
if (!editable) {
|
||||
res.push(styles.readOnly);
|
||||
}
|
||||
res.push({
|
||||
borderWidth: focusedLabel ? BORDER_FOCUSED_WIDTH : BORDER_DEFAULT_WIDTH,
|
||||
height: DEFAULT_INPUT_HEIGHT + ((focusedLabel ? BORDER_FOCUSED_WIDTH : BORDER_DEFAULT_WIDTH) * 2),
|
||||
|
|
@ -209,7 +215,7 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
|
|||
}
|
||||
|
||||
return res;
|
||||
}, [styles, theme, shouldShowError, focused, textInputStyle, focusedLabel, multiline]);
|
||||
}, [styles, theme, shouldShowError, focused, textInputStyle, focusedLabel, multiline, editable]);
|
||||
|
||||
const textAnimatedTextStyle = useAnimatedStyle(() => {
|
||||
const inputText = placeholder || value;
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -4,19 +4,16 @@
|
|||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import React from 'react';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeConfig} from '@queries/servers/system';
|
||||
import {observeConfigBooleanValue} from '@queries/servers/system';
|
||||
|
||||
import Markdown from './markdown';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const config = observeConfig(database);
|
||||
const enableLatex = config.pipe(switchMap((c) => of$(c?.EnableLatex === 'true')));
|
||||
const enableInlineLatex = config.pipe(switchMap((c) => of$(c?.EnableInlineLatex === 'true')));
|
||||
const enableLatex = observeConfigBooleanValue(database, 'EnableLatex');
|
||||
const enableInlineLatex = observeConfigBooleanValue(database, 'EnableInlineLatex');
|
||||
|
||||
return {
|
||||
enableLatex,
|
||||
|
|
|
|||
|
|
@ -3,23 +3,16 @@
|
|||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeConfig} from '@queries/servers/system';
|
||||
import {observeConfigValue} from '@queries/servers/system';
|
||||
|
||||
import MarkdownLink from './markdown_link';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhance = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const config = observeConfig(database);
|
||||
const experimentalNormalizeMarkdownLinks = config.pipe(
|
||||
switchMap((cfg) => of$(cfg?.ExperimentalNormalizeMarkdownLinks)),
|
||||
);
|
||||
const siteURL = config.pipe(
|
||||
switchMap((cfg) => of$(cfg?.SiteURL)),
|
||||
);
|
||||
const experimentalNormalizeMarkdownLinks = observeConfigValue(database, 'ExperimentalNormalizeMarkdownLinks');
|
||||
const siteURL = observeConfigValue(database, 'SiteURL');
|
||||
|
||||
return {
|
||||
experimentalNormalizeMarkdownLinks,
|
||||
|
|
|
|||
|
|
@ -23,11 +23,13 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
return {
|
||||
container: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: 32,
|
||||
height: '100%',
|
||||
alignItems: 'center' as const,
|
||||
justifyContent: 'center' as const,
|
||||
},
|
||||
result: {
|
||||
textAlign: 'center',
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 400, 'SemiBold'),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ type Props = {
|
|||
cursorPosition: number;
|
||||
rootId?: string;
|
||||
files?: FileInfo[];
|
||||
maxFileSize: number;
|
||||
maxFileCount: number;
|
||||
maxFileSize: number;
|
||||
canUploadFiles: boolean;
|
||||
updateCursorPosition: React.Dispatch<React.SetStateAction<number>>;
|
||||
updatePostInputTop: (top: number) => void;
|
||||
|
|
@ -41,8 +41,8 @@ export default function DraftHandler(props: Props) {
|
|||
cursorPosition,
|
||||
rootId = '',
|
||||
files,
|
||||
maxFileSize,
|
||||
maxFileCount,
|
||||
maxFileSize,
|
||||
canUploadFiles,
|
||||
updateCursorPosition,
|
||||
updatePostInputTop,
|
||||
|
|
@ -106,7 +106,7 @@ export default function DraftHandler(props: Props) {
|
|||
}
|
||||
|
||||
newUploadError(null);
|
||||
}, [intl, newUploadError, maxFileCount, maxFileSize, serverUrl, files?.length, channelId, rootId]);
|
||||
}, [intl, newUploadError, maxFileSize, serverUrl, files?.length, channelId, rootId]);
|
||||
|
||||
// This effect mainly handles keeping clean the uploadErrorHandlers, and
|
||||
// reinstantiate them on component mount and file retry.
|
||||
|
|
|
|||
|
|
@ -4,42 +4,23 @@
|
|||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import React from 'react';
|
||||
import {combineLatest, of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {DEFAULT_SERVER_MAX_FILE_SIZE} from '@constants/post_draft';
|
||||
import {observeConfig, observeLicense} from '@queries/servers/system';
|
||||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
import {observeCanUploadFiles, observeConfigIntValue, observeMaxFileCount} from '@queries/servers/system';
|
||||
|
||||
import DraftHandler from './draft_handler';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const config = observeConfig(database);
|
||||
|
||||
const license = observeLicense(database);
|
||||
|
||||
const canUploadFiles = combineLatest([config, license]).pipe(
|
||||
switchMap(([c, l]) => of$(
|
||||
c?.EnableFileAttachments === 'true' ||
|
||||
(l?.IsLicensed !== 'true' && l?.Compliance !== 'true' && c?.EnableMobileFileUpload === 'true'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const maxFileSize = config.pipe(
|
||||
switchMap((cfg) => of$(parseInt(cfg?.MaxFileSize || '0', 10) || DEFAULT_SERVER_MAX_FILE_SIZE)),
|
||||
);
|
||||
|
||||
const maxFileCount = config.pipe(
|
||||
switchMap((cfg) => of$(isMinimumServerVersion(cfg?.Version || '', 6, 0) ? 10 : 5)),
|
||||
);
|
||||
const canUploadFiles = observeCanUploadFiles(database);
|
||||
const maxFileSize = observeConfigIntValue(database, 'MaxFileSize', DEFAULT_SERVER_MAX_FILE_SIZE);
|
||||
const maxFileCount = observeMaxFileCount(database);
|
||||
|
||||
return {
|
||||
maxFileSize,
|
||||
maxFileCount,
|
||||
canUploadFiles,
|
||||
maxFileCount,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {of as of$} from 'rxjs';
|
|||
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
||||
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
import {observeConfig} from '@queries/servers/system';
|
||||
import {observeConfigBooleanValue, observeConfigIntValue} from '@queries/servers/system';
|
||||
|
||||
import PostInput from './post_input';
|
||||
|
||||
|
|
@ -20,18 +20,9 @@ type OwnProps = {
|
|||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({database, channelId}: WithDatabaseArgs & OwnProps) => {
|
||||
const config = observeConfig(database);
|
||||
const timeBetweenUserTypingUpdatesMilliseconds = config.pipe(
|
||||
switchMap((cfg) => of$(parseInt(cfg?.TimeBetweenUserTypingUpdatesMilliseconds || '0', 10))),
|
||||
);
|
||||
|
||||
const enableUserTypingMessage = config.pipe(
|
||||
switchMap((cfg) => of$(cfg?.EnableUserTypingMessages === 'true')),
|
||||
);
|
||||
|
||||
const maxNotificationsPerChannel = config.pipe(
|
||||
switchMap((cfg) => of$(parseInt(cfg?.MaxNotificationsPerChannel || '0', 10))),
|
||||
);
|
||||
const timeBetweenUserTypingUpdatesMilliseconds = observeConfigIntValue(database, 'TimeBetweenUserTypingUpdatesMilliseconds');
|
||||
const enableUserTypingMessage = observeConfigBooleanValue(database, 'EnableUserTypingMessages');
|
||||
const maxNotificationsPerChannel = observeConfigIntValue(database, 'MaxNotificationsPerChannel');
|
||||
|
||||
const channel = observeChannel(database, channelId);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,31 +4,16 @@
|
|||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import React from 'react';
|
||||
import {combineLatest, of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeConfig, observeLicense} from '@queries/servers/system';
|
||||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
import {observeCanUploadFiles, observeMaxFileCount} from '@queries/servers/system';
|
||||
|
||||
import QuickActions from './quick_actions';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const config = observeConfig(database);
|
||||
const license = observeLicense(database);
|
||||
|
||||
const canUploadFiles = combineLatest([config, license]).pipe(
|
||||
switchMap(([c, l]) => of$(
|
||||
c?.EnableFileAttachments === 'true' ||
|
||||
(l?.IsLicensed !== 'true' && l?.Compliance !== 'true' && c?.EnableMobileFileUpload === 'true'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const maxFileCount = config.pipe(
|
||||
switchMap((cfg) => of$(isMinimumServerVersion(cfg?.Version || '', 6, 0) ? 10 : 5)),
|
||||
);
|
||||
const canUploadFiles = observeCanUploadFiles(database);
|
||||
const maxFileCount = observeMaxFileCount(database);
|
||||
|
||||
return {
|
||||
canUploadFiles,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {MAX_MESSAGE_LENGTH_FALLBACK} from '@constants/post_draft';
|
|||
import {observeChannel, observeCurrentChannel} from '@queries/servers/channel';
|
||||
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
||||
import {observePermissionForChannel} from '@queries/servers/role';
|
||||
import {observeConfig, observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeConfigBooleanValue, observeConfigIntValue, observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeUser} from '@queries/servers/user';
|
||||
|
||||
import SendHandler from './send_handler';
|
||||
|
|
@ -42,16 +42,9 @@ const enhanced = withObservables([], (ownProps: WithDatabaseArgs & OwnProps) =>
|
|||
switchMap((u) => of$(u?.status === General.OUT_OF_OFFICE)),
|
||||
);
|
||||
|
||||
const config = observeConfig(database);
|
||||
const enableConfirmNotificationsToChannel = config.pipe(
|
||||
switchMap((cfg) => of$(Boolean(cfg?.EnableConfirmNotificationsToChannel === 'true'))),
|
||||
);
|
||||
const isTimezoneEnabled = config.pipe(
|
||||
switchMap((cfg) => of$(Boolean(cfg?.ExperimentalTimezone === 'true'))),
|
||||
);
|
||||
const maxMessageLength = config.pipe(
|
||||
switchMap((cfg) => of$(parseInt(cfg?.MaxPostSize || '0', 10) || MAX_MESSAGE_LENGTH_FALLBACK)),
|
||||
);
|
||||
const enableConfirmNotificationsToChannel = observeConfigBooleanValue(database, 'EnableConfirmNotificationsToChannel');
|
||||
const isTimezoneEnabled = observeConfigBooleanValue(database, 'ExperimentalTimezone');
|
||||
const maxMessageLength = observeConfigIntValue(database, 'MaxPostSize', MAX_MESSAGE_LENGTH_FALLBACK);
|
||||
|
||||
const useChannelMentions = combineLatest([channel, currentUser]).pipe(
|
||||
switchMap(([c, u]) => {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {ActivityIndicator, DeviceEventEmitter, Platform, View, ViewToken} from 'react-native';
|
||||
import {ActivityIndicator, DeviceEventEmitter, View, ViewToken} from 'react-native';
|
||||
import Animated, {interpolate, useAnimatedStyle, useSharedValue, withSpring} from 'react-native-reanimated';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {resetMessageCount} from '@actions/local/channel';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
|
|
@ -36,7 +37,7 @@ type Props = {
|
|||
}
|
||||
|
||||
const HIDDEN_TOP = -60;
|
||||
const SHOWN_TOP = Platform.select({ios: 50, default: 5});
|
||||
const SHOWN_TOP = 5;
|
||||
const MIN_INPUT = 0;
|
||||
const MAX_INPUT = 1;
|
||||
|
||||
|
|
@ -112,6 +113,7 @@ const MoreMessages = ({
|
|||
}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const isTablet = useIsTablet();
|
||||
const insets = useSafeAreaInsets();
|
||||
const pressed = useRef(false);
|
||||
const resetting = useRef(false);
|
||||
const initialScroll = useRef(false);
|
||||
|
|
@ -120,9 +122,11 @@ const MoreMessages = ({
|
|||
const underlayColor = useMemo(() => `hsl(${hexToHue(theme.buttonBg)}, 50%, 38%)`, [theme]);
|
||||
const top = useSharedValue(0);
|
||||
const adjustedShownTop = SHOWN_TOP + (currentCallBarVisible ? CURRENT_CALL_BAR_HEIGHT : 0) + (joinCallBannerVisible ? JOIN_CALL_BAR_HEIGHT : 0);
|
||||
const shownTop = isTablet || (isCRTEnabled && rootId) ? 5 : adjustedShownTop;
|
||||
const adjustTop = isTablet || (isCRTEnabled && rootId);
|
||||
const shownTop = adjustTop ? SHOWN_TOP : adjustedShownTop;
|
||||
const BARS_FACTOR = Math.abs((1) / (HIDDEN_TOP - SHOWN_TOP));
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{
|
||||
translateY: withSpring(interpolate(
|
||||
|
|
@ -136,13 +140,13 @@ const MoreMessages = ({
|
|||
[
|
||||
HIDDEN_TOP,
|
||||
HIDDEN_TOP,
|
||||
shownTop,
|
||||
shownTop,
|
||||
shownTop + (adjustTop ? 0 : insets.top),
|
||||
shownTop + (adjustTop ? 0 : insets.top),
|
||||
],
|
||||
Animated.Extrapolate.CLAMP,
|
||||
), {damping: 15}),
|
||||
}],
|
||||
}), [isTablet, shownTop]);
|
||||
}), [shownTop, insets.top, adjustTop]);
|
||||
|
||||
// Due to the implementation differences "unreadCount" gets updated for a channel on reset but not for a thread.
|
||||
// So we maintain a localUnreadCount to hide the indicator when the count is reset.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {of as of$, combineLatest} from 'rxjs';
|
|||
import {Preferences} from '@constants';
|
||||
import {getPreferenceAsBool} from '@helpers/api/preference';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {observeConfig} from '@queries/servers/system';
|
||||
import {observeConfigBooleanValue} from '@queries/servers/system';
|
||||
|
||||
import Opengraph from './opengraph';
|
||||
|
||||
|
|
@ -21,12 +21,12 @@ const enhance = withObservables(
|
|||
return {showLinkPreviews: of$(false)};
|
||||
}
|
||||
|
||||
const config = observeConfig(database);
|
||||
const linkPreviewsConfig = observeConfigBooleanValue(database, 'EnableLinkPreviews');
|
||||
const linkPreviewPreference = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.LINK_PREVIEW_DISPLAY).
|
||||
observeWithColumns(['value']);
|
||||
const showLinkPreviews = combineLatest([config, linkPreviewPreference], (cfg, pref) => {
|
||||
const showLinkPreviews = combineLatest([linkPreviewsConfig, linkPreviewPreference], (cfg, pref) => {
|
||||
const previewsEnabled = getPreferenceAsBool(pref, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.LINK_PREVIEW_DISPLAY, true);
|
||||
return of$(previewsEnabled && cfg?.EnableLinkPreviews === 'true');
|
||||
return of$(previewsEnabled && cfg);
|
||||
});
|
||||
|
||||
return {showLinkPreviews};
|
||||
|
|
|
|||
|
|
@ -157,9 +157,10 @@ const ProgressiveImage = ({
|
|||
opacity={defaultOpacity}
|
||||
source={{uri: thumbnailUri}}
|
||||
style={[
|
||||
thumbnailUri ? StyleSheet.absoluteFill : {tintColor: theme.centerChannelColor},
|
||||
thumbnailUri ? StyleSheet.absoluteFill : undefined,
|
||||
imageStyle,
|
||||
]}
|
||||
tintColor={thumbnailUri ? undefined : theme.centerChannelColor}
|
||||
/>
|
||||
{image}
|
||||
</Animated.View>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleProp, StyleSheet} from 'react-native';
|
||||
import {ColorValue, StyleProp} from 'react-native';
|
||||
import FastImage, {ImageStyle, Source} from 'react-native-fast-image';
|
||||
import Animated, {SharedValue} from 'react-native-reanimated';
|
||||
|
||||
|
|
@ -14,9 +14,10 @@ type ThumbnailProps = {
|
|||
opacity?: SharedValue<number>;
|
||||
source?: Source;
|
||||
style: StyleProp<ImageStyle>;
|
||||
tintColor?: ColorValue;
|
||||
}
|
||||
|
||||
const Thumbnail = ({onError, opacity, style, source}: ThumbnailProps) => {
|
||||
const Thumbnail = ({onError, opacity, style, source, tintColor}: ThumbnailProps) => {
|
||||
if (source?.uri) {
|
||||
return (
|
||||
<AnimatedFastImage
|
||||
|
|
@ -29,8 +30,6 @@ const Thumbnail = ({onError, opacity, style, source}: ThumbnailProps) => {
|
|||
);
|
||||
}
|
||||
|
||||
const tintColor = StyleSheet.flatten(style).tintColor;
|
||||
|
||||
return (
|
||||
<AnimatedFastImage
|
||||
resizeMode='contain'
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ type Props = {
|
|||
enableEmoji?: boolean;
|
||||
enableHardBreak?: boolean;
|
||||
enableSoftBreak?: boolean;
|
||||
textStyle: StyleProp<TextStyle>;
|
||||
textStyle?: StyleProp<TextStyle>;
|
||||
value: string;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ function RadioSetting({
|
|||
isSelected={value === entryValue}
|
||||
text={text}
|
||||
value={entryValue}
|
||||
key={value}
|
||||
key={entryValue}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import FormattedTime from '@components/formatted_time';
|
|||
import {Preferences} from '@constants';
|
||||
import {getPreferenceAsBool} from '@helpers/api/preference';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {observeConfig} from '@queries/servers/system';
|
||||
import {observeConfigBooleanValue} from '@queries/servers/system';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
|
@ -80,10 +80,9 @@ const SystemHeader = ({isMilitaryTime, isTimezoneEnabled, createAt, theme, user}
|
|||
};
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const config = observeConfig(database);
|
||||
const preferences = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time').
|
||||
observeWithColumns(['value']);
|
||||
const isTimezoneEnabled = config.pipe(map((cfg) => cfg?.ExperimentalTimezone === 'true'));
|
||||
const isTimezoneEnabled = observeConfigBooleanValue(database, 'ExperimentalTimezone');
|
||||
const isMilitaryTime = preferences.pipe(
|
||||
map((prefs) => getPreferenceAsBool(prefs, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', false)),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import React, {useMemo} from 'react';
|
|||
import {StyleProp, Text, TextStyle, useWindowDimensions, View, ViewStyle} from 'react-native';
|
||||
import Animated, {AnimatedStyleProp} from 'react-native-reanimated';
|
||||
|
||||
import {useIsTablet} from '@app/hooks/device';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
|
|
|
|||
|
|
@ -3,23 +3,16 @@
|
|||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeConfig, observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeConfigBooleanValue, observeCurrentUserId} from '@queries/servers/system';
|
||||
|
||||
import UserItem from './user_item';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const config = observeConfig(database);
|
||||
const isCustomStatusEnabled = config.pipe(
|
||||
switchMap((cfg) => of$(cfg?.EnableCustomUserStatuses === 'true')),
|
||||
);
|
||||
const showFullName = config.pipe(
|
||||
switchMap((cfg) => of$(cfg?.ShowFullName === 'true')),
|
||||
);
|
||||
const isCustomStatusEnabled = observeConfigBooleanValue(database, 'EnableCustomUserStatuses');
|
||||
const showFullName = observeConfigBooleanValue(database, 'ShowFullName');
|
||||
const currentUserId = observeCurrentUserId(database);
|
||||
return {
|
||||
isCustomStatusEnabled,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export const MM_TABLES = {
|
|||
CHANNEL: 'Channel',
|
||||
CHANNEL_INFO: 'ChannelInfo',
|
||||
CHANNEL_MEMBERSHIP: 'ChannelMembership',
|
||||
CONFIG: 'Config',
|
||||
CUSTOM_EMOJI: 'CustomEmoji',
|
||||
DRAFT: 'Draft',
|
||||
FILE: 'File',
|
||||
|
|
|
|||
|
|
@ -31,5 +31,4 @@ export default keyMirror({
|
|||
SEND_TO_POST_DRAFT: null,
|
||||
CRT_TOGGLED: null,
|
||||
JOIN_CALL_BAR_VISIBLE: null,
|
||||
CURRENT_CALL_BAR_VISIBLE: null,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export const SEARCH_INPUT_MARGIN = 5;
|
|||
|
||||
export const JOIN_CALL_BAR_HEIGHT = 38;
|
||||
export const CURRENT_CALL_BAR_HEIGHT = 74;
|
||||
export const CALL_ERROR_BAR_HEIGHT = 62;
|
||||
|
||||
export const QUICK_OPTIONS_HEIGHT = 270;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {DatabaseType, MIGRATION_EVENTS, MM_TABLES} from '@constants/database';
|
|||
import AppDatabaseMigrations from '@database/migration/app';
|
||||
import ServerDatabaseMigrations from '@database/migration/server';
|
||||
import {InfoModel, GlobalModel, ServersModel} from '@database/models/app';
|
||||
import {CategoryModel, CategoryChannelModel, ChannelModel, ChannelInfoModel, ChannelMembershipModel, CustomEmojiModel, DraftModel, FileModel,
|
||||
import {CategoryModel, CategoryChannelModel, ChannelModel, ChannelInfoModel, ChannelMembershipModel, ConfigModel, CustomEmojiModel, DraftModel, FileModel,
|
||||
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
|
||||
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
||||
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
||||
|
|
@ -47,7 +47,7 @@ class DatabaseManager {
|
|||
constructor() {
|
||||
this.appModels = [InfoModel, GlobalModel, ServersModel];
|
||||
this.serverModels = [
|
||||
CategoryModel, CategoryChannelModel, ChannelModel, ChannelInfoModel, ChannelMembershipModel, CustomEmojiModel, DraftModel, FileModel,
|
||||
CategoryModel, CategoryChannelModel, ChannelModel, ChannelInfoModel, ChannelMembershipModel, ConfigModel, CustomEmojiModel, DraftModel, FileModel,
|
||||
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
|
||||
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
||||
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {DeviceEventEmitter, Platform} from 'react-native';
|
|||
import DeviceInfo from 'react-native-device-info';
|
||||
import FileSystem from 'react-native-fs';
|
||||
|
||||
import {DatabaseType, MIGRATION_EVENTS, MM_TABLES} from '@constants/database';
|
||||
import {DatabaseType, MIGRATION_EVENTS, MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import AppDatabaseMigrations from '@database/migration/app';
|
||||
import ServerDatabaseMigrations from '@database/migration/server';
|
||||
import {InfoModel, GlobalModel, ServersModel} from '@database/models/app';
|
||||
|
|
@ -16,13 +16,14 @@ import {CategoryModel, CategoryChannelModel, ChannelModel, ChannelInfoModel, Cha
|
|||
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
|
||||
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
||||
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
||||
ThreadModel, ThreadParticipantModel, ThreadInTeamModel, UserModel,
|
||||
ThreadModel, ThreadParticipantModel, ThreadInTeamModel, UserModel, ConfigModel,
|
||||
} from '@database/models/server';
|
||||
import AppDataOperator from '@database/operator/app_data_operator';
|
||||
import ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import {schema as appSchema} from '@database/schema/app';
|
||||
import {serverSchema} from '@database/schema/server';
|
||||
import {queryActiveServer, queryServer, queryServerByIdentifier} from '@queries/app/servers';
|
||||
import {querySystemValue} from '@queries/servers/system';
|
||||
import {deleteLegacyFileCache} from '@utils/file';
|
||||
import {emptyFunction} from '@utils/general';
|
||||
import {logDebug, logError} from '@utils/log';
|
||||
|
|
@ -45,7 +46,7 @@ class DatabaseManager {
|
|||
constructor() {
|
||||
this.appModels = [InfoModel, GlobalModel, ServersModel];
|
||||
this.serverModels = [
|
||||
CategoryModel, CategoryChannelModel, ChannelModel, ChannelInfoModel, ChannelMembershipModel, CustomEmojiModel, DraftModel, FileModel,
|
||||
CategoryModel, CategoryChannelModel, ChannelModel, ChannelInfoModel, ChannelMembershipModel, ConfigModel, CustomEmojiModel, DraftModel, FileModel,
|
||||
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
|
||||
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
||||
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
||||
|
|
@ -109,7 +110,7 @@ class DatabaseManager {
|
|||
|
||||
return this.appDatabase;
|
||||
} catch (e) {
|
||||
// TODO : report to sentry? Show something on the UI ?
|
||||
logError('Unable to create the App Database!!', e);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
|
@ -163,8 +164,6 @@ class DatabaseManager {
|
|||
|
||||
return serverDatabase;
|
||||
} catch (e) {
|
||||
// TODO : report to sentry? Show something on the UI ?
|
||||
|
||||
logError('Error initializing database', e);
|
||||
}
|
||||
}
|
||||
|
|
@ -178,13 +177,38 @@ class DatabaseManager {
|
|||
* @returns {Promise<void>}
|
||||
*/
|
||||
private initServerDatabase = async (serverUrl: string): Promise<void> => {
|
||||
await this.createServerDatabase({
|
||||
const serverDatabase = await this.createServerDatabase({
|
||||
config: {
|
||||
dbName: serverUrl,
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl,
|
||||
},
|
||||
});
|
||||
|
||||
// Migration for config
|
||||
if (serverDatabase) {
|
||||
const {database, operator} = serverDatabase;
|
||||
const oldConfigList = await querySystemValue(database, SYSTEM_IDENTIFIERS.CONFIG).fetch();
|
||||
if (oldConfigList.length) {
|
||||
const oldConfigModel = oldConfigList[0];
|
||||
const oldConfig = oldConfigModel.value as ClientConfig;
|
||||
|
||||
const configs = [];
|
||||
let k: keyof ClientConfig;
|
||||
for (k in oldConfig) {
|
||||
// Check to silence eslint (guard-for-in)
|
||||
if (Object.prototype.hasOwnProperty.call(oldConfig, k)) {
|
||||
configs.push({
|
||||
id: k,
|
||||
value: oldConfig[k],
|
||||
});
|
||||
}
|
||||
}
|
||||
const models = await operator.handleConfigs({configs, configsToDelete: [], prepareRecordsOnly: true});
|
||||
|
||||
operator.batchRecords([...models, oldConfigModel.prepareDestroyPermanently()]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -223,7 +247,7 @@ class DatabaseManager {
|
|||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// TODO : report to sentry? Show something on the UI ?
|
||||
logError('Error adding server to App database', e);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,17 +4,36 @@
|
|||
// NOTE : To implement migration, please follow this document
|
||||
// https://nozbe.github.io/WatermelonDB/Advanced/Migrations.html
|
||||
|
||||
import {schemaMigrations, addColumns} from '@nozbe/watermelondb/Schema/migrations';
|
||||
import {schemaMigrations, addColumns, createTable} from '@nozbe/watermelondb/Schema/migrations';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {tableSchemaSpec as configSpec} from '@database/schema/server/table_schemas/config';
|
||||
|
||||
const {SERVER: {
|
||||
GROUP,
|
||||
MY_CHANNEL,
|
||||
TEAM,
|
||||
THREAD,
|
||||
}} = MM_TABLES;
|
||||
|
||||
export default schemaMigrations({migrations: [
|
||||
{
|
||||
toVersion: 5,
|
||||
steps: [
|
||||
createTable(configSpec),
|
||||
],
|
||||
},
|
||||
{
|
||||
toVersion: 4,
|
||||
steps: [
|
||||
addColumns({
|
||||
table: TEAM,
|
||||
columns: [
|
||||
{name: 'invite_id', type: 'string'},
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
toVersion: 3,
|
||||
steps: [
|
||||
|
|
|
|||
23
app/database/models/server/config.ts
Normal file
23
app/database/models/server/config.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Model} from '@nozbe/watermelondb';
|
||||
import {field} from '@nozbe/watermelondb/decorators';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
import type ConfigModelInterface from '@typings/database/models/servers/config';
|
||||
|
||||
const {CONFIG} = MM_TABLES.SERVER;
|
||||
|
||||
/**
|
||||
* The Config model is another set of key-value pair combination but this one
|
||||
* will hold the server configuration.
|
||||
*/
|
||||
export default class ConfigModel extends Model implements ConfigModelInterface {
|
||||
/** table (name) : Config */
|
||||
static table = CONFIG;
|
||||
|
||||
/** value : The value for that config/information and whose key will be the id column */
|
||||
@field('value') value!: string;
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ export {default as CategoryChannelModel} from './category_channel';
|
|||
export {default as ChannelInfoModel} from './channel_info';
|
||||
export {default as ChannelMembershipModel} from './channel_membership';
|
||||
export {default as ChannelModel} from './channel';
|
||||
export {default as ConfigModel} from './config';
|
||||
export {default as CustomEmojiModel} from './custom_emoji';
|
||||
export {default as DraftModel} from './draft';
|
||||
export {default as FileModel} from './file';
|
||||
|
|
|
|||
|
|
@ -87,6 +87,9 @@ export default class TeamModel extends Model implements TeamModelInterface {
|
|||
/** allowed_domains : List of domains that can join this team */
|
||||
@field('allowed_domains') allowedDomains!: string;
|
||||
|
||||
/** invite_id : The token id to use in invites to the team */
|
||||
@field('invite_id') inviteId!: string;
|
||||
|
||||
/** categories : All the categories associated with this team */
|
||||
@children(CATEGORY) categories!: Query<CategoryModel>;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {
|
||||
transformConfigRecord,
|
||||
transformCustomEmojiRecord,
|
||||
transformRoleRecord,
|
||||
transformSystemRecord,
|
||||
|
|
@ -95,6 +96,30 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('=> HandleConfig: should write to the CONFIG table', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
|
||||
|
||||
const configs = [{id: 'config-1', value: 'config-1'}];
|
||||
const configsToDelete = [{id: 'toDelete', value: 'toDelete'}];
|
||||
|
||||
await operator.handleConfigs({
|
||||
configs,
|
||||
configsToDelete,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
expect(spyOnHandleRecords).toHaveBeenCalledWith({
|
||||
fieldName: 'id',
|
||||
transformer: transformConfigRecord,
|
||||
createOrUpdateRawValues: configs,
|
||||
tableName: 'Config',
|
||||
prepareRecordsOnly: false,
|
||||
deleteRawValues: configsToDelete,
|
||||
});
|
||||
});
|
||||
|
||||
it('=> No table name: should not call execute if tableName is invalid', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import {MM_TABLES} from '@constants/database';
|
||||
import BaseDataOperator from '@database/operator/base_data_operator';
|
||||
import {
|
||||
transformConfigRecord,
|
||||
transformCustomEmojiRecord,
|
||||
transformRoleRecord,
|
||||
transformSystemRecord,
|
||||
|
|
@ -12,12 +13,12 @@ import {getUniqueRawsBy} from '@database/operator/utils/general';
|
|||
import {logWarning} from '@utils/log';
|
||||
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
import type {HandleCustomEmojiArgs, HandleRoleArgs, HandleSystemArgs, OperationArgs} from '@typings/database/database';
|
||||
import type {HandleConfigArgs, HandleCustomEmojiArgs, HandleRoleArgs, HandleSystemArgs, OperationArgs} from '@typings/database/database';
|
||||
import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
|
||||
import type RoleModel from '@typings/database/models/servers/role';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
|
||||
const {SERVER: {CUSTOM_EMOJI, ROLE, SYSTEM}} = MM_TABLES;
|
||||
const {SERVER: {CONFIG, CUSTOM_EMOJI, ROLE, SYSTEM}} = MM_TABLES;
|
||||
|
||||
export default class ServerDataOperatorBase extends BaseDataOperator {
|
||||
handleRole = async ({roles, prepareRecordsOnly = true}: HandleRoleArgs) => {
|
||||
|
|
@ -71,6 +72,24 @@ export default class ServerDataOperatorBase extends BaseDataOperator {
|
|||
}) as Promise<SystemModel[]>;
|
||||
};
|
||||
|
||||
handleConfigs = async ({configs, configsToDelete, prepareRecordsOnly = true}: HandleConfigArgs) => {
|
||||
if (!configs?.length && !configsToDelete?.length) {
|
||||
logWarning(
|
||||
'An empty or undefined "configs" and "configsToDelete" arrays has been passed to the handleConfigs',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.handleRecords({
|
||||
fieldName: 'id',
|
||||
transformer: transformConfigRecord,
|
||||
prepareRecordsOnly,
|
||||
createOrUpdateRawValues: getUniqueRawsBy({raws: configs, key: 'id'}),
|
||||
tableName: CONFIG,
|
||||
deleteRawValues: configsToDelete,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* execute: Handles the Create/Update operations on an table.
|
||||
* @param {OperationArgs} execute
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {MM_TABLES, OperationType} from '@constants/database';
|
|||
import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers/index';
|
||||
|
||||
import type {TransformerArgs} from '@typings/database/database';
|
||||
import type ConfigModel from '@typings/database/models/servers/config';
|
||||
import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
|
||||
import type RoleModel from '@typings/database/models/servers/role';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
|
|
@ -13,6 +14,7 @@ const {
|
|||
CUSTOM_EMOJI,
|
||||
ROLE,
|
||||
SYSTEM,
|
||||
CONFIG,
|
||||
} = MM_TABLES.SERVER;
|
||||
|
||||
/**
|
||||
|
|
@ -94,3 +96,28 @@ export const transformSystemRecord = ({action, database, value}: TransformerArgs
|
|||
fieldsMapper,
|
||||
}) as Promise<SystemModel>;
|
||||
};
|
||||
|
||||
/**
|
||||
* transformConfigRecord: Prepares a record of the SERVER database 'Config' table for update or create actions.
|
||||
* @param {TransformerArgs} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordPair} operator.value
|
||||
* @returns {Promise<ConfigModel>}
|
||||
*/
|
||||
export const transformConfigRecord = ({action, database, value}: TransformerArgs): Promise<ConfigModel> => {
|
||||
const raw = value.raw as IdValue;
|
||||
|
||||
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
|
||||
const fieldsMapper = (config: ConfigModel) => {
|
||||
config._raw.id = raw?.id;
|
||||
config.value = raw?.value as string;
|
||||
};
|
||||
|
||||
return prepareBaseRecord({
|
||||
action,
|
||||
database,
|
||||
tableName: CONFIG,
|
||||
value,
|
||||
fieldsMapper,
|
||||
}) as Promise<ConfigModel>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ export const transformTeamRecord = ({action, database, value}: TransformerArgs):
|
|||
team.allowedDomains = raw.allowed_domains;
|
||||
team.isGroupConstrained = Boolean(raw.group_constrained);
|
||||
team.lastTeamIconUpdatedAt = raw.last_team_icon_update;
|
||||
team.inviteId = raw.invite_id;
|
||||
};
|
||||
|
||||
return prepareBaseRecord({
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
ChannelInfoSchema,
|
||||
ChannelMembershipSchema,
|
||||
ChannelSchema,
|
||||
ConfigSchema,
|
||||
CustomEmojiSchema,
|
||||
DraftSchema,
|
||||
FileSchema,
|
||||
|
|
@ -37,13 +38,14 @@ import {
|
|||
} from './table_schemas';
|
||||
|
||||
export const serverSchema: AppSchema = appSchema({
|
||||
version: 3,
|
||||
version: 5,
|
||||
tables: [
|
||||
CategorySchema,
|
||||
CategoryChannelSchema,
|
||||
ChannelInfoSchema,
|
||||
ChannelMembershipSchema,
|
||||
ChannelSchema,
|
||||
ConfigSchema,
|
||||
CustomEmojiSchema,
|
||||
DraftSchema,
|
||||
FileSchema,
|
||||
|
|
|
|||
19
app/database/schema/server/table_schemas/config.ts
Normal file
19
app/database/schema/server/table_schemas/config.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {tableSchema} from '@nozbe/watermelondb';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
import type {TableSchemaSpec} from '@nozbe/watermelondb/Schema';
|
||||
|
||||
const {CONFIG} = MM_TABLES.SERVER;
|
||||
|
||||
export const tableSchemaSpec: TableSchemaSpec = {
|
||||
name: CONFIG,
|
||||
columns: [
|
||||
{name: 'value', type: 'string'},
|
||||
],
|
||||
};
|
||||
|
||||
export default tableSchema(tableSchemaSpec);
|
||||
|
|
@ -31,3 +31,4 @@ export {default as ThreadSchema} from './thread';
|
|||
export {default as ThreadParticipantSchema} from './thread_participant';
|
||||
export {default as ThreadInTeamSchema} from './thread_in_team';
|
||||
export {default as UserSchema} from './user';
|
||||
export {default as ConfigSchema} from './config';
|
||||
|
|
|
|||
|
|
@ -19,5 +19,6 @@ export default tableSchema({
|
|||
{name: 'name', type: 'string'},
|
||||
{name: 'type', type: 'string'},
|
||||
{name: 'update_at', type: 'number'},
|
||||
{name: 'invite_id', type: 'string'},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const {
|
|||
CHANNEL,
|
||||
CHANNEL_INFO,
|
||||
CHANNEL_MEMBERSHIP,
|
||||
CONFIG,
|
||||
CUSTOM_EMOJI,
|
||||
DRAFT,
|
||||
FILE,
|
||||
|
|
@ -43,7 +44,8 @@ const {
|
|||
describe('*** Test schema for SERVER database ***', () => {
|
||||
it('=> The SERVER SCHEMA should strictly match', () => {
|
||||
expect(serverSchema).toEqual({
|
||||
version: 3,
|
||||
version: 5,
|
||||
unsafeSql: undefined,
|
||||
tables: {
|
||||
[CATEGORY]: {
|
||||
name: CATEGORY,
|
||||
|
|
@ -145,6 +147,16 @@ describe('*** Test schema for SERVER database ***', () => {
|
|||
{name: 'scheme_admin', type: 'boolean'},
|
||||
],
|
||||
},
|
||||
[CONFIG]: {
|
||||
name: CONFIG,
|
||||
unsafeSql: undefined,
|
||||
columns: {
|
||||
value: {name: 'value', type: 'string'},
|
||||
},
|
||||
columnArray: [
|
||||
{name: 'value', type: 'string'},
|
||||
],
|
||||
},
|
||||
[CUSTOM_EMOJI]: {
|
||||
name: CUSTOM_EMOJI,
|
||||
unsafeSql: undefined,
|
||||
|
|
@ -463,6 +475,7 @@ describe('*** Test schema for SERVER database ***', () => {
|
|||
name: {name: 'name', type: 'string'},
|
||||
type: {name: 'type', type: 'string'},
|
||||
update_at: {name: 'update_at', type: 'number'},
|
||||
invite_id: {name: 'invite_id', type: 'string'},
|
||||
},
|
||||
columnArray: [
|
||||
{name: 'allowed_domains', type: 'string'},
|
||||
|
|
@ -474,6 +487,7 @@ describe('*** Test schema for SERVER database ***', () => {
|
|||
{name: 'name', type: 'string'},
|
||||
{name: 'type', type: 'string'},
|
||||
{name: 'update_at', type: 'number'},
|
||||
{name: 'invite_id', type: 'string'},
|
||||
],
|
||||
},
|
||||
[TEAM_CHANNEL_HISTORY]: {
|
||||
|
|
|
|||
|
|
@ -29,13 +29,13 @@ export function getPreferenceAsInt(preferences: PreferenceType[] | PreferenceMod
|
|||
return defaultValue;
|
||||
}
|
||||
|
||||
export function getTeammateNameDisplaySetting(preferences: PreferenceType[] | PreferenceModel[], config?: ClientConfig, license?: ClientLicense) {
|
||||
const useAdminTeammateNameDisplaySetting = license?.LockTeammateNameDisplay === 'true' && config?.LockTeammateNameDisplay === 'true';
|
||||
export function getTeammateNameDisplaySetting(preferences: PreferenceType[] | PreferenceModel[], lockTeammateNameDisplay?: string, teammateNameDisplay?: string, license?: ClientLicense) {
|
||||
const useAdminTeammateNameDisplaySetting = license?.LockTeammateNameDisplay === 'true' && lockTeammateNameDisplay === 'true';
|
||||
const preference = getPreferenceValue(preferences, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT, '') as string;
|
||||
if (preference && !useAdminTeammateNameDisplaySetting) {
|
||||
return preference;
|
||||
} else if (config?.TeammateNameDisplay) {
|
||||
return config.TeammateNameDisplay;
|
||||
} else if (teammateNameDisplay) {
|
||||
return teammateNameDisplay;
|
||||
}
|
||||
|
||||
return General.TEAMMATE_NAME_DISPLAY.SHOW_USERNAME;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import Emm from '@mattermost/react-native-emm';
|
||||
import deepEqual from 'deep-equal';
|
||||
import JailMonkey from 'jail-monkey';
|
||||
import {Alert, AlertButton, AppState, AppStateStatus, Platform} from 'react-native';
|
||||
|
||||
|
|
@ -18,9 +19,15 @@ class ManagedApp {
|
|||
previousAppState?: AppStateStatus;
|
||||
processConfigTimeout?: NodeJS.Timeout;
|
||||
vendor = 'Mattermost';
|
||||
cacheConfig?: ManagedConfig = undefined;
|
||||
|
||||
constructor() {
|
||||
Emm.addListener(this.processConfig);
|
||||
Emm.addListener((cfg: ManagedConfig) => {
|
||||
if (!deepEqual(cfg, this.cacheConfig)) {
|
||||
this.processConfig(cfg);
|
||||
this.cacheConfig = cfg;
|
||||
}
|
||||
});
|
||||
|
||||
this.setIOSAppGroupIdentifier();
|
||||
|
||||
|
|
@ -28,7 +35,8 @@ class ManagedApp {
|
|||
}
|
||||
|
||||
init() {
|
||||
this.processConfig(Emm.getManagedConfig<ManagedConfig>());
|
||||
this.cacheConfig = Emm.getManagedConfig<ManagedConfig>();
|
||||
this.processConfig(this.cacheConfig);
|
||||
}
|
||||
|
||||
setIOSAppGroupIdentifier = () => {
|
||||
|
|
@ -72,9 +80,9 @@ class ManagedApp {
|
|||
return;
|
||||
}
|
||||
|
||||
const inAppPinCode = config!.inAppPinCode === 'true';
|
||||
if (inAppPinCode) {
|
||||
this.handleDeviceAuthentication();
|
||||
this.inAppPinCode = config!.inAppPinCode === 'true';
|
||||
if (this.inAppPinCode && !this.performingAuthentication) {
|
||||
await this.handleDeviceAuthentication();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -111,11 +119,14 @@ class ManagedApp {
|
|||
|
||||
if (authExpired) {
|
||||
try {
|
||||
await Emm.authenticate({
|
||||
const auth = await Emm.authenticate({
|
||||
reason: translations[t('mobile.managed.secured_by')].replace('{vendor}', this.vendor),
|
||||
fallback: true,
|
||||
supressEnterPassword: true,
|
||||
});
|
||||
if (!auth) {
|
||||
throw new Error('Authorization cancelled');
|
||||
}
|
||||
} catch (err) {
|
||||
Emm.exitApp();
|
||||
return;
|
||||
|
|
@ -133,7 +144,7 @@ class ManagedApp {
|
|||
const isActive = appState === 'active';
|
||||
const isBackground = appState === 'background';
|
||||
|
||||
if (isActive && this.previousAppState === 'background') {
|
||||
if (isActive && this.previousAppState === 'background' && !this.performingAuthentication) {
|
||||
if (this.enabled && this.inAppPinCode) {
|
||||
const authExpired = this.backgroundSince > 0 && (Date.now() - this.backgroundSince) >= PROMPT_IN_APP_PIN_CODE_AFTER;
|
||||
await this.handleDeviceAuthentication(authExpired);
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ class NetworkManager {
|
|||
|
||||
private buildConfig = async () => {
|
||||
const userAgent = UserAgent.getUserAgent();
|
||||
const managedConfig = ManagedApp.enabled ? Emm.getManagedConfig<ManagedConfig>() : undefined;
|
||||
const headers: Record<string, string> = {
|
||||
...this.DEFAULT_CONFIG.headers,
|
||||
[ClientConstants.HEADER_USER_AGENT]: userAgent,
|
||||
|
|
@ -103,20 +104,15 @@ class NetworkManager {
|
|||
|
||||
const config = {
|
||||
...this.DEFAULT_CONFIG,
|
||||
sessionConfiguration: {
|
||||
...this.DEFAULT_CONFIG.sessionConfiguration,
|
||||
timeoutIntervalForRequest: managedConfig?.timeout ? parseInt(managedConfig.timeout, 10) : this.DEFAULT_CONFIG.sessionConfiguration.timeoutIntervalForRequest,
|
||||
timeoutIntervalForResource: managedConfig?.timeoutVPN ? parseInt(managedConfig.timeoutVPN, 10) : this.DEFAULT_CONFIG.sessionConfiguration.timeoutIntervalForResource,
|
||||
waitsForConnectivity: managedConfig?.useVPN === 'true',
|
||||
},
|
||||
headers,
|
||||
};
|
||||
|
||||
if (ManagedApp.enabled) {
|
||||
const managedConfig = Emm.getManagedConfig<ManagedConfig>();
|
||||
if (managedConfig?.useVPN === 'true') {
|
||||
config.sessionConfiguration.waitsForConnectivity = true;
|
||||
}
|
||||
|
||||
if (managedConfig?.timeoutVPN) {
|
||||
config.sessionConfiguration.timeoutIntervalForResource = parseInt(managedConfig.timeoutVPN, 10);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,9 @@ import {AppState, AppStateStatus, DeviceEventEmitter, Platform} from 'react-nati
|
|||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import {cancelSessionNotification, logout, scheduleSessionNotification} from '@actions/remote/session';
|
||||
import {resetMomentLocale} from '@app/i18n';
|
||||
import {deleteFileCache, deleteFileCacheByDir} from '@app/utils/file';
|
||||
import {Events, Launch} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {resetMomentLocale} from '@i18n';
|
||||
import {getAllServerCredentials, removeServerCredentials} from '@init/credentials';
|
||||
import {relaunchApp} from '@init/launch';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
|
|
@ -20,6 +19,7 @@ import {queryServerName} from '@queries/app/servers';
|
|||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {getThemeFromState, resetToOnboarding} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {deleteFileCache, deleteFileCacheByDir} from '@utils/file';
|
||||
import {addNewServer} from '@utils/server';
|
||||
|
||||
import type {LaunchType} from '@typings/launch';
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import {getConnectionForTesting} from '@calls/actions/calls';
|
|||
import * as Permissions from '@calls/actions/permissions';
|
||||
import * as State from '@calls/state';
|
||||
import {
|
||||
myselfLeftCall,
|
||||
newCurrentCall,
|
||||
setCallsConfig,
|
||||
setCallsState,
|
||||
setChannelsWithCalls,
|
||||
|
|
@ -139,7 +141,10 @@ describe('Actions.Calls', () => {
|
|||
|
||||
let response: { data?: string };
|
||||
await act(async () => {
|
||||
response = await CallsActions.joinCall('server1', 'channel-id');
|
||||
response = await CallsActions.joinCall('server1', 'channel-id', 'myUserId', true);
|
||||
|
||||
// manually call newCurrentConnection because newConnection is mocked
|
||||
newCurrentCall('server1', 'channel-id', 'myUserId');
|
||||
userJoinedCall('server1', 'channel-id', 'myUserId');
|
||||
});
|
||||
|
||||
|
|
@ -163,7 +168,10 @@ describe('Actions.Calls', () => {
|
|||
|
||||
let response: { data?: string };
|
||||
await act(async () => {
|
||||
response = await CallsActions.joinCall('server1', 'channel-id');
|
||||
response = await CallsActions.joinCall('server1', 'channel-id', 'myUserId', true);
|
||||
|
||||
// manually call newCurrentConnection because newConnection is mocked
|
||||
newCurrentCall('server1', 'channel-id', 'myUserId');
|
||||
userJoinedCall('server1', 'channel-id', 'myUserId');
|
||||
});
|
||||
assert.equal(response!.data, 'channel-id');
|
||||
|
|
@ -174,6 +182,9 @@ describe('Actions.Calls', () => {
|
|||
|
||||
await act(async () => {
|
||||
CallsActions.leaveCall();
|
||||
|
||||
// because disconnect is mocked
|
||||
myselfLeftCall();
|
||||
});
|
||||
|
||||
expect(disconnectMock).toBeCalled();
|
||||
|
|
@ -191,7 +202,10 @@ describe('Actions.Calls', () => {
|
|||
|
||||
let response: { data?: string };
|
||||
await act(async () => {
|
||||
response = await CallsActions.joinCall('server1', 'channel-id');
|
||||
response = await CallsActions.joinCall('server1', 'channel-id', 'myUserId', true);
|
||||
|
||||
// manually call newCurrentConnection because newConnection is mocked
|
||||
newCurrentCall('server1', 'channel-id', 'myUserId');
|
||||
userJoinedCall('server1', 'channel-id', 'myUserId');
|
||||
});
|
||||
assert.equal(response!.data, 'channel-id');
|
||||
|
|
@ -218,7 +232,10 @@ describe('Actions.Calls', () => {
|
|||
|
||||
let response: { data?: string };
|
||||
await act(async () => {
|
||||
response = await CallsActions.joinCall('server1', 'channel-id');
|
||||
response = await CallsActions.joinCall('server1', 'channel-id', 'mysUserId', true);
|
||||
|
||||
// manually call newCurrentConnection because newConnection is mocked
|
||||
newCurrentCall('server1', 'channel-id', 'myUserId');
|
||||
userJoinedCall('server1', 'channel-id', 'myUserId');
|
||||
});
|
||||
assert.equal(response!.data, 'channel-id');
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {fetchUsersByIds} from '@actions/remote/user';
|
|||
import {
|
||||
getCallsConfig,
|
||||
getCallsState,
|
||||
myselfLeftCall,
|
||||
setCalls,
|
||||
setChannelEnabled,
|
||||
setConfig,
|
||||
|
|
@ -16,6 +15,8 @@ import {
|
|||
setScreenShareURL,
|
||||
setSpeakerPhone,
|
||||
setCallForChannel,
|
||||
newCurrentCall,
|
||||
myselfLeftCall,
|
||||
} from '@calls/state';
|
||||
import {General, Preferences} from '@constants';
|
||||
import Calls from '@constants/calls';
|
||||
|
|
@ -24,7 +25,7 @@ import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
|||
import NetworkManager from '@managers/network_manager';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {getCommonSystemValues} from '@queries/servers/system';
|
||||
import {getConfig, getLicense} from '@queries/servers/system';
|
||||
import {getCurrentUser, getUserById} from '@queries/servers/user';
|
||||
import {displayUsername, getUserIdFromChannelName, isSystemAdmin} from '@utils/user';
|
||||
|
||||
|
|
@ -218,7 +219,7 @@ export const enableChannelCalls = async (serverUrl: string, channelId: string, e
|
|||
return {};
|
||||
};
|
||||
|
||||
export const joinCall = async (serverUrl: string, channelId: string): Promise<{ error?: string | Error; data?: string }> => {
|
||||
export const joinCall = async (serverUrl: string, channelId: string, userId: string, hasMicPermission: boolean): Promise<{ error?: string | Error; data?: string }> => {
|
||||
// Edge case: calls was disabled when app loaded, and then enabled, but app hasn't
|
||||
// reconnected its websocket since then (i.e., hasn't called batchLoadCalls yet)
|
||||
const {data: enabled} = await checkIsCallsPluginEnabled(serverUrl);
|
||||
|
|
@ -231,9 +232,12 @@ export const joinCall = async (serverUrl: string, channelId: string): Promise<{
|
|||
connection = null;
|
||||
}
|
||||
setSpeakerphoneOn(false);
|
||||
newCurrentCall(serverUrl, channelId, userId);
|
||||
|
||||
try {
|
||||
connection = await newConnection(serverUrl, channelId, () => null, setScreenShareURL);
|
||||
connection = await newConnection(serverUrl, channelId, () => {
|
||||
myselfLeftCall();
|
||||
}, setScreenShareURL, hasMicPermission);
|
||||
} catch (error: unknown) {
|
||||
await forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error: error as Error};
|
||||
|
|
@ -255,7 +259,6 @@ export const leaveCall = () => {
|
|||
connection = null;
|
||||
}
|
||||
setSpeakerphoneOn(false);
|
||||
myselfLeftCall();
|
||||
};
|
||||
|
||||
export const muteMyself = () => {
|
||||
|
|
@ -270,6 +273,12 @@ export const unmuteMyself = () => {
|
|||
}
|
||||
};
|
||||
|
||||
export const initializeVoiceTrack = () => {
|
||||
if (connection) {
|
||||
connection.initializeVoiceTrack();
|
||||
}
|
||||
};
|
||||
|
||||
export const raiseHand = () => {
|
||||
if (connection) {
|
||||
connection.raiseHand();
|
||||
|
|
@ -337,9 +346,10 @@ export const getEndCallMessage = async (serverUrl: string, channelId: string, cu
|
|||
if (channel.type === General.DM_CHANNEL) {
|
||||
const otherID = getUserIdFromChannelName(currentUserId, channel.name);
|
||||
const otherUser = await getUserById(database, otherID);
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const displaySetting = getTeammateNameDisplaySetting(preferences, config, license);
|
||||
const displaySetting = getTeammateNameDisplaySetting(preferences, config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
msg = intl.formatMessage({
|
||||
id: 'mobile.calls_end_msg_dm',
|
||||
defaultMessage: 'Are you sure you want to end the call with {displayName}?',
|
||||
|
|
|
|||
|
|
@ -1,28 +1,10 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Alert, Platform} from 'react-native';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import {Platform} from 'react-native';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
const getMicrophonePermissionDeniedMessage = (intl: IntlShape) => {
|
||||
const {formatMessage} = intl;
|
||||
const applicationName = DeviceInfo.getApplicationName();
|
||||
return {
|
||||
title: formatMessage({
|
||||
id: 'mobile.microphone_permission_denied_title',
|
||||
defaultMessage: '{applicationName} would like to access your microphone',
|
||||
}, {applicationName}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.microphone_permission_denied_description',
|
||||
defaultMessage: 'To participate in this call, open Settings to grant Mattermost access to your microphone.',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export const hasMicrophonePermission = async (intl: IntlShape) => {
|
||||
export const hasMicrophonePermission = async () => {
|
||||
const targetSource = Platform.select({
|
||||
ios: Permissions.PERMISSIONS.IOS.MICROPHONE,
|
||||
default: Permissions.PERMISSIONS.ANDROID.RECORD_AUDIO,
|
||||
|
|
@ -36,32 +18,8 @@ export const hasMicrophonePermission = async (intl: IntlShape) => {
|
|||
|
||||
return permissionRequest === Permissions.RESULTS.GRANTED;
|
||||
}
|
||||
case Permissions.RESULTS.BLOCKED: {
|
||||
const grantOption = {
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.permission_denied_retry',
|
||||
defaultMessage: 'Settings',
|
||||
}),
|
||||
onPress: () => Permissions.openSettings(),
|
||||
};
|
||||
|
||||
const {title, text} = getMicrophonePermissionDeniedMessage(intl);
|
||||
|
||||
Alert.alert(
|
||||
title,
|
||||
text,
|
||||
[
|
||||
grantOption,
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.permission_denied_dismiss',
|
||||
defaultMessage: 'Don\'t Allow',
|
||||
}),
|
||||
},
|
||||
],
|
||||
);
|
||||
case Permissions.RESULTS.BLOCKED:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,11 @@
|
|||
import {Alert} from 'react-native';
|
||||
|
||||
import {hasMicrophonePermission, joinCall, unmuteMyself} from '@calls/actions';
|
||||
import {setMicPermissionsGranted} from '@calls/state';
|
||||
import {errorAlert} from '@calls/utils';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
|
|
@ -89,16 +93,24 @@ export const leaveAndJoinWithAlert = (
|
|||
const doJoinCall = async (serverUrl: string, channelId: string, isDMorGM: boolean, intl: IntlShape) => {
|
||||
const {formatMessage} = intl;
|
||||
|
||||
const hasPermission = await hasMicrophonePermission(intl);
|
||||
if (!hasPermission) {
|
||||
errorAlert(formatMessage({
|
||||
id: 'mobile.calls_error_permissions',
|
||||
defaultMessage: 'No permissions to microphone, unable to start call',
|
||||
}), intl);
|
||||
let user;
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
user = await getCurrentUser(database);
|
||||
if (!user) {
|
||||
// This shouldn't happen, so don't bother localizing and displaying an alert.
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
logError('failed to getServerDatabaseAndOperator in doJoinCall', error);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await joinCall(serverUrl, channelId);
|
||||
const hasPermission = await hasMicrophonePermission();
|
||||
setMicPermissionsGranted(hasPermission);
|
||||
|
||||
const res = await joinCall(serverUrl, channelId, user.id, hasPermission);
|
||||
if (res.error) {
|
||||
const seeLogs = formatMessage({id: 'mobile.calls_see_logs', defaultMessage: 'See server logs'});
|
||||
errorAlert(res.error?.toString() || seeLogs, intl);
|
||||
|
|
|
|||
|
|
@ -34,13 +34,12 @@ const enhanced = withObservables([], ({serverUrl, channelId, database}: EnhanceP
|
|||
switchMap((calls) => of$(Boolean(calls[channelId]))),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const currentCall = observeCurrentCall();
|
||||
const ccDatabase = currentCall.pipe(
|
||||
const ccDatabase = observeCurrentCall().pipe(
|
||||
switchMap((call) => of$(call?.serverUrl || '')),
|
||||
distinctUntilChanged(),
|
||||
switchMap((url) => of$(DatabaseManager.serverDatabases[url]?.database)),
|
||||
);
|
||||
const ccChannelId = currentCall.pipe(
|
||||
const ccChannelId = observeCurrentCall().pipe(
|
||||
switchMap((call) => of$(call?.channelId)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
|
@ -58,7 +57,6 @@ const enhanced = withObservables([], ({serverUrl, channelId, database}: EnhanceP
|
|||
isACallInCurrentChannel,
|
||||
confirmToJoin,
|
||||
alreadyInCall,
|
||||
currentCall,
|
||||
currentCallChannelName,
|
||||
limitRestrictedInfo: observeIsCallLimitRestricted(serverUrl, channelId),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {View, Text, TouchableOpacity, Pressable, Platform, DeviceEventEmitter} from 'react-native';
|
||||
import {View, Text, TouchableOpacity, Pressable, Platform} from 'react-native';
|
||||
import {Options} from 'react-native-navigation';
|
||||
|
||||
import {muteMyself, unmuteMyself} from '@calls/actions';
|
||||
import CallAvatar from '@calls/components/call_avatar';
|
||||
import {CurrentCall, VoiceEventData} from '@calls/types/calls';
|
||||
import PermissionErrorBar from '@calls/components/permission_error_bar';
|
||||
import UnavailableIconWrapper from '@calls/components/unavailable_icon_wrapper';
|
||||
import {usePermissionsChecker} from '@calls/hooks';
|
||||
import {CurrentCall} from '@calls/types/calls';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {Events, Screens, WebsocketEvents} from '@constants';
|
||||
import {Screens} from '@constants';
|
||||
import {CURRENT_CALL_BAR_HEIGHT} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {dismissAllModalsAndPopToScreen} from '@screens/navigation';
|
||||
|
|
@ -24,6 +27,7 @@ type Props = {
|
|||
currentCall: CurrentCall | null;
|
||||
userModelsDict: Dictionary<UserModel>;
|
||||
teammateNameDisplay: string;
|
||||
micPermissionsGranted: boolean;
|
||||
threadScreen?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -57,18 +61,18 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
color: theme.sidebarText,
|
||||
opacity: 0.64,
|
||||
},
|
||||
micIcon: {
|
||||
color: theme.sidebarText,
|
||||
micIconContainer: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
textAlign: 'center',
|
||||
textAlignVertical: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#3DB887',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.onlineIndicator,
|
||||
borderRadius: 4,
|
||||
margin: 4,
|
||||
padding: 9,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
micIcon: {
|
||||
color: theme.sidebarText,
|
||||
},
|
||||
muted: {
|
||||
backgroundColor: 'transparent',
|
||||
|
|
@ -86,49 +90,13 @@ const CurrentCallBar = ({
|
|||
currentCall,
|
||||
userModelsDict,
|
||||
teammateNameDisplay,
|
||||
micPermissionsGranted,
|
||||
threadScreen,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
const {formatMessage} = useIntl();
|
||||
const [speaker, setSpeaker] = useState<string | null>(null);
|
||||
const [talkingMessage, setTalkingMessage] = useState('');
|
||||
|
||||
const isCurrentCall = Boolean(currentCall);
|
||||
const handleVoiceOn = (data: VoiceEventData) => {
|
||||
if (data.channelId === currentCall?.channelId) {
|
||||
setSpeaker(data.userId);
|
||||
}
|
||||
};
|
||||
const handleVoiceOff = (data: VoiceEventData) => {
|
||||
if (data.channelId === currentCall?.channelId && ((speaker === data.userId) || !speaker)) {
|
||||
setSpeaker(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const onVoiceOn = DeviceEventEmitter.addListener(WebsocketEvents.CALLS_USER_VOICE_ON, handleVoiceOn);
|
||||
const onVoiceOff = DeviceEventEmitter.addListener(WebsocketEvents.CALLS_USER_VOICE_OFF, handleVoiceOff);
|
||||
DeviceEventEmitter.emit(Events.CURRENT_CALL_BAR_VISIBLE, isCurrentCall);
|
||||
return () => {
|
||||
DeviceEventEmitter.emit(Events.CURRENT_CALL_BAR_VISIBLE, Boolean(false));
|
||||
onVoiceOn.remove();
|
||||
onVoiceOff.remove();
|
||||
};
|
||||
}, [isCurrentCall]);
|
||||
|
||||
useEffect(() => {
|
||||
if (speaker) {
|
||||
setTalkingMessage(formatMessage({
|
||||
id: 'mobile.calls_name_is_talking',
|
||||
defaultMessage: '{name} is talking',
|
||||
}, {name: displayUsername(userModelsDict[speaker], teammateNameDisplay)}));
|
||||
} else {
|
||||
setTalkingMessage(formatMessage({
|
||||
id: 'mobile.calls_noone_talking',
|
||||
defaultMessage: 'No one is talking',
|
||||
}));
|
||||
}
|
||||
}, [speaker, setTalkingMessage]);
|
||||
usePermissionsChecker(micPermissionsGranted);
|
||||
|
||||
const goToCallScreen = useCallback(async () => {
|
||||
const options: Options = {
|
||||
|
|
@ -150,6 +118,21 @@ const CurrentCallBar = ({
|
|||
|
||||
const myParticipant = currentCall?.participants[currentCall.myUserId];
|
||||
|
||||
// Since we can only see one user talking, it doesn't really matter who we show here (e.g., we can't
|
||||
// tell who is speaking louder).
|
||||
const talkingUsers = Object.keys(currentCall?.voiceOn || {});
|
||||
const speaker = talkingUsers.length > 0 ? talkingUsers[0] : '';
|
||||
let talkingMessage = formatMessage({
|
||||
id: 'mobile.calls_noone_talking',
|
||||
defaultMessage: 'No one is talking',
|
||||
});
|
||||
if (speaker) {
|
||||
talkingMessage = formatMessage({
|
||||
id: 'mobile.calls_name_is_talking',
|
||||
defaultMessage: '{name} is talking',
|
||||
}, {name: displayUsername(userModelsDict[speaker], teammateNameDisplay)});
|
||||
}
|
||||
|
||||
const muteUnmute = () => {
|
||||
if (myParticipant?.muted) {
|
||||
unmuteMyself();
|
||||
|
|
@ -158,42 +141,48 @@ const CurrentCallBar = ({
|
|||
}
|
||||
};
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const micPermissionsError = !micPermissionsGranted && !currentCall?.micPermissionsErrorDismissed;
|
||||
|
||||
return (
|
||||
<View style={style.wrapper}>
|
||||
<View style={style.container}>
|
||||
<CallAvatar
|
||||
userModel={userModelsDict[speaker || '']}
|
||||
volume={speaker ? 0.5 : 0}
|
||||
serverUrl={currentCall?.serverUrl || ''}
|
||||
/>
|
||||
<View style={style.userInfo}>
|
||||
<Text style={style.speakingUser}>{talkingMessage}</Text>
|
||||
<Text style={style.currentChannel}>{`~${displayName}`}</Text>
|
||||
<>
|
||||
<View style={style.wrapper}>
|
||||
<View style={style.container}>
|
||||
<CallAvatar
|
||||
userModel={userModelsDict[speaker || '']}
|
||||
volume={speaker ? 0.5 : 0}
|
||||
serverUrl={currentCall?.serverUrl || ''}
|
||||
/>
|
||||
<View style={style.userInfo}>
|
||||
<Text style={style.speakingUser}>{talkingMessage}</Text>
|
||||
<Text style={style.currentChannel}>{`~${displayName}`}</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
onPressIn={goToCallScreen}
|
||||
style={style.pressable}
|
||||
>
|
||||
<CompassIcon
|
||||
name='arrow-expand'
|
||||
size={24}
|
||||
style={style.expandIcon}
|
||||
/>
|
||||
</Pressable>
|
||||
<TouchableOpacity
|
||||
onPress={muteUnmute}
|
||||
style={[style.pressable, style.micIconContainer, myParticipant?.muted && style.muted]}
|
||||
disabled={!micPermissionsGranted}
|
||||
>
|
||||
<UnavailableIconWrapper
|
||||
name={myParticipant?.muted ? 'microphone-off' : 'microphone'}
|
||||
size={24}
|
||||
unavailable={!micPermissionsGranted}
|
||||
style={[style.micIcon]}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Pressable
|
||||
onPressIn={goToCallScreen}
|
||||
style={style.pressable}
|
||||
>
|
||||
<CompassIcon
|
||||
name='arrow-expand'
|
||||
size={24}
|
||||
style={style.expandIcon}
|
||||
/>
|
||||
</Pressable>
|
||||
<TouchableOpacity
|
||||
onPress={muteUnmute}
|
||||
style={style.pressable}
|
||||
>
|
||||
<CompassIcon
|
||||
name={myParticipant?.muted ? 'microphone-off' : 'microphone'}
|
||||
size={24}
|
||||
style={[style.micIcon, myParticipant?.muted ? style.muted : undefined]}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
{micPermissionsError && <PermissionErrorBar/>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentCallBar;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import withObservables from '@nozbe/with-observables';
|
|||
import {combineLatest, of as of$} from 'rxjs';
|
||||
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeCurrentCall} from '@calls/state';
|
||||
import {observeCurrentCall, observeGlobalCallsState} from '@calls/state';
|
||||
import {idsAreEqual} from '@calls/utils';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
|
|
@ -45,12 +45,17 @@ const enhanced = withObservables([], () => {
|
|||
const teammateNameDisplay = database.pipe(
|
||||
switchMap((db) => (db ? observeTeammateNameDisplay(db) : of$(''))),
|
||||
);
|
||||
const micPermissionsGranted = observeGlobalCallsState().pipe(
|
||||
switchMap((gs) => of$(gs.micPermissionsGranted)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
return {
|
||||
displayName,
|
||||
currentCall,
|
||||
userModelsDict,
|
||||
teammateNameDisplay,
|
||||
micPermissionsGranted,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
104
app/products/calls/components/permission_error_bar.tsx
Normal file
104
app/products/calls/components/permission_error_bar.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Pressable, View} from 'react-native';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {setMicPermissionsErrorDismissed} from '@calls/state';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {CALL_ERROR_BAR_HEIGHT} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => (
|
||||
{
|
||||
pressable: {
|
||||
zIndex: 10,
|
||||
},
|
||||
errorWrapper: {
|
||||
padding: 10,
|
||||
paddingTop: 0,
|
||||
},
|
||||
errorBar: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: theme.dndIndicator,
|
||||
minHeight: CALL_ERROR_BAR_HEIGHT,
|
||||
width: '100%',
|
||||
borderRadius: 5,
|
||||
padding: 10,
|
||||
alignItems: 'center',
|
||||
},
|
||||
errorText: {
|
||||
flex: 1,
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
color: theme.buttonColor,
|
||||
},
|
||||
errorIconContainer: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 4,
|
||||
margin: 0,
|
||||
padding: 9,
|
||||
},
|
||||
pressedErrorIconContainer: {
|
||||
backgroundColor: theme.buttonColor,
|
||||
},
|
||||
errorIcon: {
|
||||
color: theme.buttonColor,
|
||||
fontSize: 18,
|
||||
},
|
||||
pressedErrorIcon: {
|
||||
color: theme.dndIndicator,
|
||||
},
|
||||
paddingRight: {
|
||||
paddingRight: 9,
|
||||
},
|
||||
}
|
||||
));
|
||||
|
||||
const PermissionErrorBar = () => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.errorWrapper}>
|
||||
<Pressable
|
||||
onPress={Permissions.openSettings}
|
||||
style={style.errorBar}
|
||||
>
|
||||
<CompassIcon
|
||||
name='microphone-off'
|
||||
style={[style.errorIcon, style.paddingRight]}
|
||||
/>
|
||||
<FormattedText
|
||||
id={'mobile.calls_mic_error'}
|
||||
defaultMessage={'To participate, open Settings to grant Mattermost access to your microphone.'}
|
||||
style={style.errorText}
|
||||
/>
|
||||
<Pressable
|
||||
onPress={setMicPermissionsErrorDismissed}
|
||||
hitSlop={5}
|
||||
style={({pressed}) => [
|
||||
style.pressable,
|
||||
style.errorIconContainer,
|
||||
pressed && style.pressedErrorIconContainer,
|
||||
]}
|
||||
>
|
||||
{({pressed}) => (
|
||||
<CompassIcon
|
||||
name='close'
|
||||
style={[style.errorIcon, pressed && style.pressedErrorIcon]}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PermissionErrorBar;
|
||||
68
app/products/calls/components/unavailable_icon_wrapper.tsx
Normal file
68
app/products/calls/components/unavailable_icon_wrapper.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleProp, TextStyle, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
size: number;
|
||||
style: StyleProp<TextStyle>;
|
||||
unavailable: boolean;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
position: 'relative',
|
||||
},
|
||||
unavailable: {
|
||||
color: changeOpacity(theme.sidebarText, 0.32),
|
||||
},
|
||||
errorContainer: {
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
backgroundColor: '#3F4350',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#3F4350',
|
||||
},
|
||||
errorIcon: {
|
||||
color: theme.dndIndicator,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const UnavailableIconWrapper = ({name, size, style: providedStyle, unavailable}: Props) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
const errorIconSize = size / 2;
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<CompassIcon
|
||||
name={name}
|
||||
size={size}
|
||||
style={[providedStyle, unavailable && style.unavailable]}
|
||||
/>
|
||||
{unavailable &&
|
||||
<View
|
||||
style={[style.errorContainer, {borderRadius: errorIconSize / 2}]}
|
||||
>
|
||||
<CompassIcon
|
||||
name={'close-circle'}
|
||||
size={errorIconSize}
|
||||
style={style.errorIcon}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnavailableIconWrapper;
|
||||
|
|
@ -25,7 +25,13 @@ import type {CallsConnection} from '@calls/types/calls';
|
|||
|
||||
const peerConnectTimeout = 5000;
|
||||
|
||||
export async function newConnection(serverUrl: string, channelID: string, closeCb: () => void, setScreenShareURL: (url: string) => void) {
|
||||
export async function newConnection(
|
||||
serverUrl: string,
|
||||
channelID: string,
|
||||
closeCb: () => void,
|
||||
setScreenShareURL: (url: string) => void,
|
||||
hasMicPermission: boolean,
|
||||
) {
|
||||
let peer: Peer | null = null;
|
||||
let stream: MediaStream;
|
||||
let voiceTrackAdded = false;
|
||||
|
|
@ -34,21 +40,26 @@ export async function newConnection(serverUrl: string, channelID: string, closeC
|
|||
let onCallEnd: EmitterSubscription | null = null;
|
||||
const streams: MediaStream[] = [];
|
||||
|
||||
try {
|
||||
stream = await mediaDevices.getUserMedia({
|
||||
video: false,
|
||||
audio: true,
|
||||
}) as MediaStream;
|
||||
voiceTrack = stream.getAudioTracks()[0];
|
||||
voiceTrack.enabled = false;
|
||||
streams.push(stream);
|
||||
} catch (err) {
|
||||
logError('Unable to get media device:', err);
|
||||
}
|
||||
const initializeVoiceTrack = async () => {
|
||||
if (voiceTrack) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
stream = await mediaDevices.getUserMedia({
|
||||
video: false,
|
||||
audio: true,
|
||||
}) as MediaStream;
|
||||
voiceTrack = stream.getAudioTracks()[0];
|
||||
voiceTrack.enabled = false;
|
||||
streams.push(stream);
|
||||
} catch (err) {
|
||||
logError('Unable to get media device:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// getClient can throw an error, which will be handled by the caller.
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
|
||||
const credentials = await getServerCredentials(serverUrl);
|
||||
|
||||
const ws = new WebSocketClient(serverUrl, client.getWebSocketUrl(), credentials?.token);
|
||||
|
|
@ -56,6 +67,10 @@ export async function newConnection(serverUrl: string, channelID: string, closeC
|
|||
// Throws an error, to be caught by caller.
|
||||
await ws.initialize();
|
||||
|
||||
if (hasMicPermission) {
|
||||
initializeVoiceTrack();
|
||||
}
|
||||
|
||||
const disconnect = () => {
|
||||
if (isClosed) {
|
||||
return;
|
||||
|
|
@ -182,6 +197,7 @@ export async function newConnection(serverUrl: string, channelID: string, closeC
|
|||
|
||||
InCallManager.start({media: 'audio'});
|
||||
InCallManager.stopProximitySensor();
|
||||
|
||||
peer = new Peer(null, iceConfigs);
|
||||
peer.on('signal', (data: any) => {
|
||||
if (data.type === 'offer' || data.type === 'answer') {
|
||||
|
|
@ -265,6 +281,7 @@ export async function newConnection(serverUrl: string, channelID: string, closeC
|
|||
waitForPeerConnection,
|
||||
raiseHand,
|
||||
unraiseHand,
|
||||
initializeVoiceTrack,
|
||||
};
|
||||
|
||||
return connection;
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import {encode} from '@msgpack/msgpack/dist';
|
|||
|
||||
import Calls from '@constants/calls';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getCommonSystemValues} from '@queries/servers/system';
|
||||
import {logError} from '@utils/log';
|
||||
import {getConfigValue} from '@queries/servers/system';
|
||||
import {logError, logDebug} from '@utils/log';
|
||||
|
||||
const wsMinReconnectRetryTimeMs = 1000; // 1 second
|
||||
const wsReconnectionTimeout = 30000; // 30 seconds
|
||||
|
|
@ -42,8 +42,8 @@ export class WebSocketClient extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
|
||||
const system = await getCommonSystemValues(database);
|
||||
const connectionUrl = (system.config.WebsocketURL || this.serverUrl) + this.wsPath;
|
||||
const websocketURL = await getConfigValue(database, 'WebsocketURL');
|
||||
const connectionUrl = (websocketURL || this.serverUrl) + this.wsPath;
|
||||
|
||||
this.ws = new WebSocket(`${connectionUrl}?connection_id=${this.connID}&sequence_number=${this.serverSeqNo}`, [], {headers: {authorization: `Bearer ${this.authToken}`}});
|
||||
|
||||
|
|
@ -88,9 +88,11 @@ export class WebSocketClient extends EventEmitter {
|
|||
|
||||
if (msg.event === 'hello') {
|
||||
if (msg.data.connection_id !== this.connID) {
|
||||
logDebug('calls: ws new conn id from server');
|
||||
this.connID = msg.data.connection_id;
|
||||
this.serverSeqNo = 0;
|
||||
if (this.originalConnID === '') {
|
||||
logDebug('calls: ws setting original conn id');
|
||||
this.originalConnID = this.connID;
|
||||
}
|
||||
}
|
||||
|
|
@ -99,6 +101,7 @@ export class WebSocketClient extends EventEmitter {
|
|||
}
|
||||
return;
|
||||
} else if (!this.connID) {
|
||||
logDebug('calls: ws message received while waiting for hello');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
setChannelEnabled,
|
||||
setRaisedHand,
|
||||
setUserMuted,
|
||||
setUserVoiceOn,
|
||||
userJoinedCall,
|
||||
userLeftCall,
|
||||
} from '@calls/state';
|
||||
|
|
@ -38,17 +39,11 @@ export const handleCallUserUnmuted = (serverUrl: string, msg: WebSocketMessage)
|
|||
};
|
||||
|
||||
export const handleCallUserVoiceOn = (msg: WebSocketMessage) => {
|
||||
DeviceEventEmitter.emit(WebsocketEvents.CALLS_USER_VOICE_ON, {
|
||||
channelId: msg.broadcast.channel_id,
|
||||
userId: msg.data.userID,
|
||||
});
|
||||
setUserVoiceOn(msg.broadcast.channel_id, msg.data.userID, true);
|
||||
};
|
||||
|
||||
export const handleCallUserVoiceOff = (msg: WebSocketMessage) => {
|
||||
DeviceEventEmitter.emit(WebsocketEvents.CALLS_USER_VOICE_OFF, {
|
||||
channelId: msg.broadcast.channel_id,
|
||||
userId: msg.data.userID,
|
||||
});
|
||||
setUserVoiceOn(msg.broadcast.channel_id, msg.data.userID, false);
|
||||
};
|
||||
|
||||
export const handleCallStarted = (serverUrl: string, msg: WebSocketMessage) => {
|
||||
|
|
|
|||
|
|
@ -3,14 +3,18 @@
|
|||
|
||||
// Check if calls is enabled. If it is, then run fn; if it isn't, show an alert and set
|
||||
// msgPostfix to ' (Not Available)'.
|
||||
import {useCallback, useState} from 'react';
|
||||
import {useCallback, useEffect, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
import {Alert, Platform} from 'react-native';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {initializeVoiceTrack} from '@calls/actions/calls';
|
||||
import {setMicPermissionsGranted} from '@calls/state';
|
||||
import {errorAlert} from '@calls/utils';
|
||||
import {Client} from '@client/rest';
|
||||
import ClientError from '@client/rest/error';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useAppState} from '@hooks/device';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
|
||||
export const useTryCallsFunction = (fn: () => void) => {
|
||||
|
|
@ -71,3 +75,27 @@ export const useTryCallsFunction = (fn: () => void) => {
|
|||
|
||||
return [tryFn, msgPostfix] as [() => Promise<void>, string];
|
||||
};
|
||||
|
||||
const micPermission = Platform.select({
|
||||
ios: Permissions.PERMISSIONS.IOS.MICROPHONE,
|
||||
default: Permissions.PERMISSIONS.ANDROID.RECORD_AUDIO,
|
||||
});
|
||||
|
||||
export const usePermissionsChecker = (micPermissionsGranted: boolean) => {
|
||||
const appState = useAppState();
|
||||
|
||||
useEffect(() => {
|
||||
const asyncFn = async () => {
|
||||
if (appState === 'active') {
|
||||
const hasPermission = (await Permissions.check(micPermission)) === Permissions.RESULTS.GRANTED;
|
||||
if (hasPermission) {
|
||||
initializeVoiceTrack();
|
||||
setMicPermissionsGranted(hasPermission);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (!micPermissionsGranted) {
|
||||
asyncFn();
|
||||
}
|
||||
}, [appState]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -28,9 +28,12 @@ import {
|
|||
} from '@calls/actions';
|
||||
import CallAvatar from '@calls/components/call_avatar';
|
||||
import CallDuration from '@calls/components/call_duration';
|
||||
import PermissionErrorBar from '@calls/components/permission_error_bar';
|
||||
import UnavailableIconWrapper from '@calls/components/unavailable_icon_wrapper';
|
||||
import {usePermissionsChecker} from '@calls/hooks';
|
||||
import RaisedHandIcon from '@calls/icons/raised_hand_icon';
|
||||
import UnraisedHandIcon from '@calls/icons/unraised_hand_icon';
|
||||
import {CallParticipant, CurrentCall, VoiceEventData} from '@calls/types/calls';
|
||||
import {CallParticipant, CurrentCall} from '@calls/types/calls';
|
||||
import {sortParticipants} from '@calls/utils';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
|
|
@ -48,13 +51,14 @@ import {
|
|||
import NavigationStore from '@store/navigation_store';
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
import {mergeNavigationOptions} from '@utils/navigation';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
export type Props = {
|
||||
componentId: string;
|
||||
currentCall: CurrentCall | null;
|
||||
participantsDict: Dictionary<CallParticipant>;
|
||||
micPermissionsGranted: boolean;
|
||||
teammateNameDisplay: string;
|
||||
fromThreadScreen?: boolean;
|
||||
}
|
||||
|
|
@ -252,20 +256,31 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
color: 'white',
|
||||
margin: 3,
|
||||
},
|
||||
unavailableText: {
|
||||
color: changeOpacity(theme.sidebarText, 0.32),
|
||||
},
|
||||
}));
|
||||
|
||||
const CallScreen = ({componentId, currentCall, participantsDict, teammateNameDisplay, fromThreadScreen}: Props) => {
|
||||
const CallScreen = ({
|
||||
componentId,
|
||||
currentCall,
|
||||
participantsDict,
|
||||
micPermissionsGranted,
|
||||
teammateNameDisplay,
|
||||
fromThreadScreen,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
const {width, height} = useWindowDimensions();
|
||||
usePermissionsChecker(micPermissionsGranted);
|
||||
const [showControlsInLandscape, setShowControlsInLandscape] = useState(false);
|
||||
const [speakers, setSpeakers] = useState<Dictionary<boolean>>({});
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const isLandscape = width > height;
|
||||
const showControls = !isLandscape || showControlsInLandscape;
|
||||
const myParticipant = currentCall?.participants[currentCall.myUserId];
|
||||
const micPermissionsError = !micPermissionsGranted && !currentCall?.micPermissionsErrorDismissed;
|
||||
const chatThreadTitle = intl.formatMessage({id: 'mobile.calls_chat_thread', defaultMessage: 'Chat thread'});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -279,30 +294,6 @@ const CallScreen = ({componentId, currentCall, participantsDict, teammateNameDis
|
|||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleVoiceOn = (data: VoiceEventData) => {
|
||||
if (data.channelId === currentCall?.channelId) {
|
||||
setSpeakers((prev) => ({...prev, [data.userId]: true}));
|
||||
}
|
||||
};
|
||||
const handleVoiceOff = (data: VoiceEventData) => {
|
||||
if (data.channelId === currentCall?.channelId && speakers.hasOwnProperty(data.userId)) {
|
||||
setSpeakers((prev) => {
|
||||
const next = {...prev};
|
||||
delete next[data.userId];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onVoiceOn = DeviceEventEmitter.addListener(WebsocketEvents.CALLS_USER_VOICE_ON, handleVoiceOn);
|
||||
const onVoiceOff = DeviceEventEmitter.addListener(WebsocketEvents.CALLS_USER_VOICE_OFF, handleVoiceOff);
|
||||
return () => {
|
||||
onVoiceOn.remove();
|
||||
onVoiceOff.remove();
|
||||
};
|
||||
}, [speakers, currentCall?.channelId]);
|
||||
|
||||
const leaveCallHandler = useCallback(() => {
|
||||
popTopScreen();
|
||||
leaveCall();
|
||||
|
|
@ -325,6 +316,10 @@ const CallScreen = ({componentId, currentCall, participantsDict, teammateNameDis
|
|||
}
|
||||
}, [myParticipant?.raisedHand]);
|
||||
|
||||
const toggleSpeakerPhone = useCallback(() => {
|
||||
setSpeakerphoneOn(!currentCall?.speakerphoneOn);
|
||||
}, [currentCall?.speakerphoneOn]);
|
||||
|
||||
const toggleControlsInLandscape = useCallback(() => {
|
||||
setShowControlsInLandscape(!showControlsInLandscape);
|
||||
}, [showControlsInLandscape]);
|
||||
|
|
@ -424,8 +419,8 @@ const CallScreen = ({componentId, currentCall, participantsDict, teammateNameDis
|
|||
usersList = (
|
||||
<ScrollView
|
||||
alwaysBounceVertical={false}
|
||||
horizontal={currentCall?.screenOn !== ''}
|
||||
contentContainerStyle={[isLandscape && currentCall?.screenOn && style.usersScrollLandscapeScreenOn]}
|
||||
horizontal={currentCall.screenOn !== ''}
|
||||
contentContainerStyle={[isLandscape && currentCall.screenOn && style.usersScrollLandscapeScreenOn]}
|
||||
>
|
||||
<Pressable
|
||||
testID='users-list'
|
||||
|
|
@ -435,12 +430,12 @@ const CallScreen = ({componentId, currentCall, participantsDict, teammateNameDis
|
|||
{participants.map((user) => {
|
||||
return (
|
||||
<View
|
||||
style={[style.user, currentCall?.screenOn && style.userScreenOn]}
|
||||
style={[style.user, currentCall.screenOn && style.userScreenOn]}
|
||||
key={user.id}
|
||||
>
|
||||
<CallAvatar
|
||||
userModel={user.userModel}
|
||||
volume={speakers[user.id] ? 1 : 0}
|
||||
volume={currentCall.voiceOn[user.id] ? 1 : 0}
|
||||
muted={user.muted}
|
||||
sharingScreen={user.id === currentCall.screenOn}
|
||||
raisedHand={Boolean(user.raisedHand)}
|
||||
|
|
@ -484,7 +479,7 @@ const CallScreen = ({componentId, currentCall, participantsDict, teammateNameDis
|
|||
<FormattedText
|
||||
id={'mobile.calls_unmute'}
|
||||
defaultMessage={'Unmute'}
|
||||
style={style.buttonText}
|
||||
style={[style.buttonText, !micPermissionsGranted && style.unavailableText]}
|
||||
/>);
|
||||
|
||||
return (
|
||||
|
|
@ -508,6 +503,7 @@ const CallScreen = ({componentId, currentCall, participantsDict, teammateNameDis
|
|||
</View>
|
||||
{usersList}
|
||||
{screenShareView}
|
||||
{micPermissionsError && <PermissionErrorBar/>}
|
||||
<View
|
||||
style={[style.buttons, isLandscape && style.buttonsLandscape, !showControls && style.buttonsLandscapeNoControls]}
|
||||
>
|
||||
|
|
@ -516,10 +512,12 @@ const CallScreen = ({componentId, currentCall, participantsDict, teammateNameDis
|
|||
testID='mute-unmute'
|
||||
style={[style.mute, myParticipant.muted && style.muteMuted]}
|
||||
onPress={muteUnmuteHandler}
|
||||
disabled={!micPermissionsGranted}
|
||||
>
|
||||
<CompassIcon
|
||||
<UnavailableIconWrapper
|
||||
name={myParticipant.muted ? 'microphone-off' : 'microphone'}
|
||||
size={24}
|
||||
unavailable={!micPermissionsGranted}
|
||||
style={style.muteIcon}
|
||||
/>
|
||||
{myParticipant.muted ? UnmuteText : MuteText}
|
||||
|
|
@ -544,12 +542,12 @@ const CallScreen = ({componentId, currentCall, participantsDict, teammateNameDis
|
|||
<Pressable
|
||||
testID={'toggle-speakerphone'}
|
||||
style={style.button}
|
||||
onPress={() => setSpeakerphoneOn(!currentCall?.speakerphoneOn)}
|
||||
onPress={toggleSpeakerPhone}
|
||||
>
|
||||
<CompassIcon
|
||||
name={'volume-high'}
|
||||
size={24}
|
||||
style={[style.buttonIcon, style.speakerphoneIcon, currentCall?.speakerphoneOn && style.speakerphoneIconOn]}
|
||||
style={[style.buttonIcon, style.speakerphoneIcon, currentCall.speakerphoneOn && style.speakerphoneIconOn]}
|
||||
/>
|
||||
<FormattedText
|
||||
id={'mobile.calls_speaker'}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {combineLatest, of as of$} from 'rxjs';
|
|||
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
|
||||
|
||||
import CallScreen from '@calls/screens/call_screen/call_screen';
|
||||
import {observeCurrentCall} from '@calls/state';
|
||||
import {observeCurrentCall, observeGlobalCallsState} from '@calls/state';
|
||||
import {CallParticipant} from '@calls/types/calls';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {observeTeammateNameDisplay, queryUsersById} from '@queries/servers/user';
|
||||
|
|
@ -34,6 +34,10 @@ const enhanced = withObservables([], () => {
|
|||
}, {} as Dictionary<CallParticipant>))),
|
||||
)),
|
||||
);
|
||||
const micPermissionsGranted = observeGlobalCallsState().pipe(
|
||||
switchMap((gs) => of$(gs.micPermissionsGranted)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const teammateNameDisplay = database.pipe(
|
||||
switchMap((db) => (db ? observeTeammateNameDisplay(db) : of$(''))),
|
||||
distinctUntilChanged(),
|
||||
|
|
@ -42,6 +46,7 @@ const enhanced = withObservables([], () => {
|
|||
return {
|
||||
currentCall,
|
||||
participantsDict,
|
||||
micPermissionsGranted,
|
||||
teammateNameDisplay,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,13 +6,17 @@ import assert from 'assert';
|
|||
import {act, renderHook} from '@testing-library/react-hooks';
|
||||
|
||||
import {
|
||||
newCurrentCall,
|
||||
setCallsState,
|
||||
setChannelsWithCalls,
|
||||
setCurrentCall,
|
||||
setMicPermissionsErrorDismissed,
|
||||
setMicPermissionsGranted,
|
||||
useCallsConfig,
|
||||
useCallsState,
|
||||
useChannelsWithCalls,
|
||||
useCurrentCall,
|
||||
useGlobalCallsState,
|
||||
} from '@calls/state';
|
||||
import {
|
||||
setCalls,
|
||||
|
|
@ -30,12 +34,22 @@ import {
|
|||
setSpeakerPhone,
|
||||
setConfig,
|
||||
setPluginEnabled,
|
||||
setUserVoiceOn,
|
||||
} from '@calls/state/actions';
|
||||
import {License} from '@constants';
|
||||
|
||||
import {CallsState, CurrentCall, DefaultCallsConfig, DefaultCallsState} from '../types/calls';
|
||||
import {
|
||||
Call,
|
||||
CallsState,
|
||||
CurrentCall,
|
||||
DefaultCallsConfig,
|
||||
DefaultCallsState,
|
||||
DefaultCurrentCall,
|
||||
DefaultGlobalCallsState,
|
||||
GlobalCallsState,
|
||||
} from '../types/calls';
|
||||
|
||||
const call1 = {
|
||||
const call1: Call = {
|
||||
participants: {
|
||||
'user-1': {id: 'user-1', muted: false, raisedHand: 0},
|
||||
'user-2': {id: 'user-2', muted: true, raisedHand: 0},
|
||||
|
|
@ -46,7 +60,7 @@ const call1 = {
|
|||
threadId: 'thread-1',
|
||||
ownerId: 'user-1',
|
||||
};
|
||||
const call2 = {
|
||||
const call2: Call = {
|
||||
participants: {
|
||||
'user-3': {id: 'user-3', muted: false, raisedHand: 0},
|
||||
'user-4': {id: 'user-4', muted: true, raisedHand: 0},
|
||||
|
|
@ -57,7 +71,7 @@ const call2 = {
|
|||
threadId: 'thread-2',
|
||||
ownerId: 'user-3',
|
||||
};
|
||||
const call3 = {
|
||||
const call3: Call = {
|
||||
participants: {
|
||||
'user-5': {id: 'user-5', muted: false, raisedHand: 0},
|
||||
'user-6': {id: 'user-6', muted: true, raisedHand: 0},
|
||||
|
|
@ -107,39 +121,67 @@ describe('useCallsState', () => {
|
|||
const initialChannelsWithCallsState = {
|
||||
'channel-1': true,
|
||||
};
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
};
|
||||
const testNewCall1 = {
|
||||
...call1,
|
||||
participants: {
|
||||
'user-1': {id: 'user-1', muted: false, raisedHand: 0},
|
||||
'user-2': {id: 'user-2', muted: true, raisedHand: 0},
|
||||
'user-3': {id: 'user-3', muted: false, raisedHand: 123},
|
||||
},
|
||||
};
|
||||
const test = {
|
||||
calls: {'channel-1': call2, 'channel-2': call3},
|
||||
calls: {'channel-1': testNewCall1, 'channel-2': call2, 'channel-3': call3},
|
||||
enabled: {'channel-2': true},
|
||||
};
|
||||
|
||||
const expectedCallsState = {
|
||||
...initialCallsState,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myId',
|
||||
calls: {'channel-1': call2, 'channel-2': call3},
|
||||
calls: {'channel-1': testNewCall1, 'channel-2': call2, 'channel-3': call3},
|
||||
enabled: {'channel-2': true},
|
||||
};
|
||||
const expectedChannelsWithCallsState = {
|
||||
...initialChannelsWithCallsState,
|
||||
'channel-2': true,
|
||||
'channel-3': true,
|
||||
};
|
||||
const expectedCurrentCallState = {
|
||||
...initialCurrentCallState,
|
||||
...testNewCall1,
|
||||
};
|
||||
|
||||
// setup
|
||||
const {result} = renderHook(() => {
|
||||
return [useCallsState('server1'), useCallsState('server1'), useChannelsWithCalls('server1')];
|
||||
return [
|
||||
useCallsState('server1'),
|
||||
useCallsState('server1'),
|
||||
useChannelsWithCalls('server1'),
|
||||
useCurrentCall(),
|
||||
];
|
||||
});
|
||||
act(() => {
|
||||
setCallsState('server1', initialCallsState);
|
||||
setChannelsWithCalls('server1', initialChannelsWithCallsState);
|
||||
setCurrentCall(initialCurrentCallState);
|
||||
});
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
assert.deepEqual(result.current[1], initialCallsState);
|
||||
assert.deepEqual(result.current[2], initialChannelsWithCallsState);
|
||||
assert.deepEqual(result.current[3], initialCurrentCallState);
|
||||
|
||||
// test
|
||||
act(() => setCalls('server1', 'myId', test.calls, test.enabled));
|
||||
assert.deepEqual(result.current[0], expectedCallsState);
|
||||
assert.deepEqual(result.current[1], expectedCallsState);
|
||||
assert.deepEqual(result.current[2], expectedChannelsWithCallsState);
|
||||
assert.deepEqual(result.current[3], expectedCurrentCallState);
|
||||
});
|
||||
|
||||
it('joinedCall', () => {
|
||||
|
|
@ -150,13 +192,14 @@ describe('useCallsState', () => {
|
|||
const initialChannelsWithCallsState = {
|
||||
'channel-1': true,
|
||||
};
|
||||
const initialCurrentCallState = {
|
||||
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
connected: true,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
} as CurrentCall;
|
||||
};
|
||||
const expectedCallsState = {
|
||||
'channel-1': {
|
||||
participants: {
|
||||
|
|
@ -209,13 +252,13 @@ describe('useCallsState', () => {
|
|||
const initialChannelsWithCallsState = {
|
||||
'channel-1': true,
|
||||
};
|
||||
const initialCurrentCallState = {
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
connected: true,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
} as CurrentCall;
|
||||
};
|
||||
const expectedCallsState = {
|
||||
'channel-1': {
|
||||
participants: {
|
||||
|
|
@ -311,13 +354,13 @@ describe('useCallsState', () => {
|
|||
calls: {'channel-1': call1, 'channel-2': call2},
|
||||
};
|
||||
const initialChannelsWithCallsState = {'channel-1': true, 'channel-2': true};
|
||||
const initialCurrentCallState = {
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
connected: true,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
} as CurrentCall;
|
||||
};
|
||||
|
||||
// setup
|
||||
const {result} = renderHook(() => {
|
||||
|
|
@ -358,13 +401,12 @@ describe('useCallsState', () => {
|
|||
calls: {'channel-1': call1, 'channel-2': call2},
|
||||
};
|
||||
const initialChannelsWithCallsState = {'channel-1': true, 'channel-2': true};
|
||||
const initialCurrentCallState = {
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
} as CurrentCall;
|
||||
};
|
||||
|
||||
// setup
|
||||
const {result} = renderHook(() => {
|
||||
|
|
@ -416,13 +458,13 @@ describe('useCallsState', () => {
|
|||
ownerId: 'user-1',
|
||||
},
|
||||
};
|
||||
const initialCurrentCallState = {
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
connected: true,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
} as CurrentCall;
|
||||
};
|
||||
const expectedCurrentCallState = {
|
||||
...initialCurrentCallState,
|
||||
...expectedCalls['channel-1'],
|
||||
|
|
@ -469,17 +511,18 @@ describe('useCallsState', () => {
|
|||
};
|
||||
const expectedCallsState = {
|
||||
...initialCallsState,
|
||||
calls: {...initialCallsState.calls,
|
||||
calls: {
|
||||
...initialCallsState.calls,
|
||||
'channel-1': newCall1,
|
||||
},
|
||||
};
|
||||
const expectedCurrentCallState = {
|
||||
const expectedCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
connected: true,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
...newCall1,
|
||||
} as CurrentCall;
|
||||
};
|
||||
|
||||
// setup
|
||||
const {result} = renderHook(() => {
|
||||
|
|
@ -490,7 +533,10 @@ describe('useCallsState', () => {
|
|||
assert.deepEqual(result.current[1], null);
|
||||
|
||||
// test
|
||||
act(() => userJoinedCall('server1', 'channel-1', 'myUserId'));
|
||||
act(() => {
|
||||
newCurrentCall('server1', 'channel-1', 'myUserId');
|
||||
userJoinedCall('server1', 'channel-1', 'myUserId');
|
||||
});
|
||||
assert.deepEqual(result.current[0], expectedCallsState);
|
||||
assert.deepEqual(result.current[1], expectedCurrentCallState);
|
||||
|
||||
|
|
@ -560,6 +606,7 @@ describe('useCallsState', () => {
|
|||
assert.deepEqual(result.current[1], null);
|
||||
|
||||
// test joining a call and setting url:
|
||||
act(() => newCurrentCall('server1', 'channel-1', 'myUserId'));
|
||||
act(() => userJoinedCall('server1', 'channel-1', 'myUserId'));
|
||||
assert.deepEqual((result.current[1] as CurrentCall | null)?.screenShareURL, '');
|
||||
act(() => setScreenShareURL('testUrl'));
|
||||
|
|
@ -589,7 +636,8 @@ describe('useCallsState', () => {
|
|||
};
|
||||
const expectedCallsState = {
|
||||
...initialCallsState,
|
||||
calls: {...initialCallsState.calls,
|
||||
calls: {
|
||||
...initialCallsState.calls,
|
||||
'channel-1': newCall1,
|
||||
},
|
||||
};
|
||||
|
|
@ -603,6 +651,7 @@ describe('useCallsState', () => {
|
|||
assert.deepEqual(result.current[1], null);
|
||||
|
||||
// test
|
||||
act(() => newCurrentCall('server1', 'channel-1', 'myUserId'));
|
||||
act(() => userJoinedCall('server1', 'channel-1', 'myUserId'));
|
||||
assert.deepEqual((result.current[1] as CurrentCall | null)?.speakerphoneOn, false);
|
||||
act(() => setSpeakerPhone(true));
|
||||
|
|
@ -618,6 +667,123 @@ describe('useCallsState', () => {
|
|||
assert.deepEqual(result.current[1], null);
|
||||
});
|
||||
|
||||
it('MicPermissions', () => {
|
||||
const initialGlobalState = DefaultGlobalCallsState;
|
||||
const initialCallsState: CallsState = {
|
||||
...DefaultCallsState,
|
||||
myUserId: 'myUserId',
|
||||
calls: {'channel-1': call1, 'channel-2': call2},
|
||||
};
|
||||
const newCall1: Call = {
|
||||
...call1,
|
||||
participants: {
|
||||
...call1.participants,
|
||||
myUserId: {id: 'myUserId', muted: true, raisedHand: 0},
|
||||
},
|
||||
};
|
||||
const expectedCallsState: CallsState = {
|
||||
...initialCallsState,
|
||||
calls: {
|
||||
...initialCallsState.calls,
|
||||
'channel-1': newCall1,
|
||||
},
|
||||
};
|
||||
const expectedCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
connected: true,
|
||||
...newCall1,
|
||||
};
|
||||
const secondExpectedCurrentCallState: CurrentCall = {
|
||||
...expectedCurrentCallState,
|
||||
micPermissionsErrorDismissed: true,
|
||||
};
|
||||
const expectedGlobalState: GlobalCallsState = {
|
||||
micPermissionsGranted: true,
|
||||
};
|
||||
|
||||
// setup
|
||||
const {result} = renderHook(() => {
|
||||
return [useCallsState('server1'), useCurrentCall(), useGlobalCallsState()];
|
||||
});
|
||||
act(() => setCallsState('server1', initialCallsState));
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
assert.deepEqual(result.current[1], null);
|
||||
assert.deepEqual(result.current[2], initialGlobalState);
|
||||
|
||||
// join call
|
||||
act(() => {
|
||||
setMicPermissionsGranted(false);
|
||||
newCurrentCall('server1', 'channel-1', 'myUserId');
|
||||
userJoinedCall('server1', 'channel-1', 'myUserId');
|
||||
});
|
||||
assert.deepEqual(result.current[0], expectedCallsState);
|
||||
assert.deepEqual(result.current[1], expectedCurrentCallState);
|
||||
assert.deepEqual(result.current[2], initialGlobalState);
|
||||
|
||||
// dismiss mic error
|
||||
act(() => setMicPermissionsErrorDismissed());
|
||||
assert.deepEqual(result.current[0], expectedCallsState);
|
||||
assert.deepEqual(result.current[1], secondExpectedCurrentCallState);
|
||||
assert.deepEqual(result.current[2], initialGlobalState);
|
||||
|
||||
// grant permissions
|
||||
act(() => setMicPermissionsGranted(true));
|
||||
assert.deepEqual(result.current[0], expectedCallsState);
|
||||
assert.deepEqual(result.current[1], secondExpectedCurrentCallState);
|
||||
assert.deepEqual(result.current[2], expectedGlobalState);
|
||||
|
||||
act(() => {
|
||||
myselfLeftCall();
|
||||
userLeftCall('server1', 'channel-1', 'myUserId');
|
||||
});
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
assert.deepEqual(result.current[1], null);
|
||||
});
|
||||
|
||||
it('voiceOn and Off', () => {
|
||||
const initialCallsState = {
|
||||
...DefaultCallsState,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
calls: {'channel-1': call1, 'channel-2': call2},
|
||||
};
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
};
|
||||
|
||||
// setup
|
||||
const {result} = renderHook(() => {
|
||||
return [useCallsState('server1'), useCurrentCall()];
|
||||
});
|
||||
act(() => {
|
||||
setCallsState('server1', initialCallsState);
|
||||
setCurrentCall(initialCurrentCallState);
|
||||
});
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
assert.deepEqual(result.current[1], initialCurrentCallState);
|
||||
|
||||
// test
|
||||
act(() => setUserVoiceOn('channel-1', 'user-1', true));
|
||||
assert.deepEqual(result.current[1], {...initialCurrentCallState, voiceOn: {'user-1': true}});
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
act(() => setUserVoiceOn('channel-1', 'user-2', true));
|
||||
assert.deepEqual(result.current[1], {...initialCurrentCallState, voiceOn: {'user-1': true, 'user-2': true}});
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
act(() => setUserVoiceOn('channel-1', 'user-1', false));
|
||||
assert.deepEqual(result.current[1], {...initialCurrentCallState, voiceOn: {'user-2': true}});
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
|
||||
// test that voice state is cleared on reconnect
|
||||
act(() => setCalls('server1', 'myUserId', initialCallsState.calls, {}));
|
||||
assert.deepEqual(result.current[1], initialCurrentCallState);
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
});
|
||||
|
||||
it('config', () => {
|
||||
const newConfig = {
|
||||
ICEServers: [],
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ import {
|
|||
getCallsState,
|
||||
getChannelsWithCalls,
|
||||
getCurrentCall,
|
||||
getGlobalCallsState,
|
||||
setCallsConfig,
|
||||
setCallsState,
|
||||
setChannelsWithCalls,
|
||||
setCurrentCall,
|
||||
setGlobalCallsState,
|
||||
} from '@calls/state';
|
||||
import {Call, CallsConfig, ChannelsWithCalls} from '@calls/types/calls';
|
||||
import {Call, CallsConfig, ChannelsWithCalls, DefaultCall, DefaultCurrentCall} from '@calls/types/calls';
|
||||
|
||||
export const setCalls = (serverUrl: string, myUserId: string, calls: Dictionary<Call>, enabled: Dictionary<boolean>) => {
|
||||
const channelsWithCalls = Object.keys(calls).reduce(
|
||||
|
|
@ -22,6 +24,21 @@ export const setCalls = (serverUrl: string, myUserId: string, calls: Dictionary<
|
|||
setChannelsWithCalls(serverUrl, channelsWithCalls);
|
||||
|
||||
setCallsState(serverUrl, {serverUrl, myUserId, calls, enabled});
|
||||
|
||||
// Does the current call need to be updated?
|
||||
const currentCall = getCurrentCall();
|
||||
if (!currentCall || !calls[currentCall.channelId]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Edge case: if the app went into the background and lost the main ws connection, we don't know who is currently
|
||||
// talking. Instead of guessing, erase voiceOn state (same state as when joining an ongoing call).
|
||||
const nextCall = {
|
||||
...currentCall,
|
||||
...calls[currentCall.channelId],
|
||||
voiceOn: {},
|
||||
};
|
||||
setCurrentCall(nextCall);
|
||||
};
|
||||
|
||||
export const setCallForChannel = (serverUrl: string, channelId: string, enabled: boolean, call?: Call) => {
|
||||
|
|
@ -80,23 +97,21 @@ export const userJoinedCall = (serverUrl: string, channelId: string, userId: str
|
|||
// Did the user join the current call? If so, update that too.
|
||||
const currentCall = getCurrentCall();
|
||||
if (currentCall && currentCall.channelId === channelId) {
|
||||
const voiceOn = {...currentCall.voiceOn};
|
||||
delete voiceOn[userId];
|
||||
|
||||
const nextCurrentCall = {
|
||||
...currentCall,
|
||||
participants: {...currentCall.participants, [userId]: nextCall.participants[userId]},
|
||||
voiceOn,
|
||||
};
|
||||
setCurrentCall(nextCurrentCall);
|
||||
}
|
||||
|
||||
// Was it me that joined the call?
|
||||
if (callsState.myUserId === userId) {
|
||||
setCurrentCall({
|
||||
...nextCall,
|
||||
participants: {...nextCall.participants},
|
||||
serverUrl,
|
||||
myUserId: userId,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
});
|
||||
// If this is the currentUser, that means we've connected to the call we created.
|
||||
if (userId === nextCurrentCall.myUserId) {
|
||||
nextCurrentCall.connected = true;
|
||||
}
|
||||
|
||||
setCurrentCall(nextCurrentCall);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -137,14 +152,35 @@ export const userLeftCall = (serverUrl: string, channelId: string, userId: strin
|
|||
return;
|
||||
}
|
||||
|
||||
// Clear them from the voice list
|
||||
const voiceOn = {...currentCall.voiceOn};
|
||||
delete voiceOn[userId];
|
||||
|
||||
const nextCurrentCall = {
|
||||
...currentCall,
|
||||
participants: {...currentCall.participants},
|
||||
voiceOn,
|
||||
};
|
||||
delete nextCurrentCall.participants[userId];
|
||||
setCurrentCall(nextCurrentCall);
|
||||
};
|
||||
|
||||
export const newCurrentCall = (serverUrl: string, channelId: string, myUserId: string) => {
|
||||
let existingCall: Call = DefaultCall;
|
||||
const callsState = getCallsState(serverUrl);
|
||||
if (callsState.calls[channelId]) {
|
||||
existingCall = callsState.calls[channelId];
|
||||
}
|
||||
|
||||
setCurrentCall({
|
||||
...DefaultCurrentCall,
|
||||
...existingCall,
|
||||
serverUrl,
|
||||
channelId,
|
||||
myUserId,
|
||||
});
|
||||
};
|
||||
|
||||
export const myselfLeftCall = () => {
|
||||
setCurrentCall(null);
|
||||
};
|
||||
|
|
@ -157,6 +193,21 @@ export const callStarted = (serverUrl: string, call: Call) => {
|
|||
|
||||
const nextChannelsWithCalls = {...getChannelsWithCalls(serverUrl), [call.channelId]: true};
|
||||
setChannelsWithCalls(serverUrl, nextChannelsWithCalls);
|
||||
|
||||
// If we started a call, we will get a callStarted event with the 'official' data from the server.
|
||||
// Save that in our currentCall.
|
||||
const currentCall = getCurrentCall();
|
||||
if (!currentCall || currentCall.channelId !== call.channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextCurrentCall = {
|
||||
...currentCall,
|
||||
startTime: call.startTime,
|
||||
threadId: call.threadId,
|
||||
ownerId: call.ownerId,
|
||||
};
|
||||
setCurrentCall(nextCurrentCall);
|
||||
};
|
||||
|
||||
export const callEnded = (serverUrl: string, channelId: string) => {
|
||||
|
|
@ -170,10 +221,7 @@ export const callEnded = (serverUrl: string, channelId: string) => {
|
|||
delete nextChannelsWithCalls[channelId];
|
||||
setChannelsWithCalls(serverUrl, nextChannelsWithCalls);
|
||||
|
||||
const currentCall = getCurrentCall();
|
||||
if (currentCall?.channelId === channelId) {
|
||||
setCurrentCall(null);
|
||||
}
|
||||
// currentCall is set to null by the disconnect.
|
||||
};
|
||||
|
||||
export const setUserMuted = (serverUrl: string, channelId: string, userId: string, muted: boolean) => {
|
||||
|
|
@ -208,6 +256,26 @@ export const setUserMuted = (serverUrl: string, channelId: string, userId: strin
|
|||
setCurrentCall(nextCurrentCall);
|
||||
};
|
||||
|
||||
export const setUserVoiceOn = (channelId: string, userId: string, voiceOn: boolean) => {
|
||||
const currentCall = getCurrentCall();
|
||||
if (!currentCall || currentCall.channelId !== channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextVoiceOn = {...currentCall.voiceOn};
|
||||
if (voiceOn) {
|
||||
nextVoiceOn[userId] = true;
|
||||
} else {
|
||||
delete nextVoiceOn[userId];
|
||||
}
|
||||
|
||||
const nextCurrentCall = {
|
||||
...currentCall,
|
||||
voiceOn: nextVoiceOn,
|
||||
};
|
||||
setCurrentCall(nextCurrentCall);
|
||||
};
|
||||
|
||||
export const setRaisedHand = (serverUrl: string, channelId: string, userId: string, timestamp: number) => {
|
||||
const callsState = getCallsState(serverUrl);
|
||||
if (!callsState.calls[channelId] || !callsState.calls[channelId].participants[userId]) {
|
||||
|
|
@ -318,3 +386,26 @@ export const setPluginEnabled = (serverUrl: string, pluginEnabled: boolean) => {
|
|||
const callsConfig = getCallsConfig(serverUrl);
|
||||
setCallsConfig(serverUrl, {...callsConfig, pluginEnabled});
|
||||
};
|
||||
|
||||
export const setMicPermissionsGranted = (granted: boolean) => {
|
||||
const globalState = getGlobalCallsState();
|
||||
|
||||
const nextGlobalState = {
|
||||
...globalState,
|
||||
micPermissionsGranted: granted,
|
||||
};
|
||||
setGlobalCallsState(nextGlobalState);
|
||||
};
|
||||
|
||||
export const setMicPermissionsErrorDismissed = () => {
|
||||
const currentCall = getCurrentCall();
|
||||
if (!currentCall) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextCurrentCall = {
|
||||
...currentCall,
|
||||
micPermissionsErrorDismissed: true,
|
||||
};
|
||||
setCurrentCall(nextCurrentCall);
|
||||
};
|
||||
|
|
|
|||
38
app/products/calls/state/global_calls_state.ts
Normal file
38
app/products/calls/state/global_calls_state.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
import {DefaultGlobalCallsState, GlobalCallsState} from '@calls/types/calls';
|
||||
|
||||
const globalStateSubject = new BehaviorSubject(DefaultGlobalCallsState);
|
||||
|
||||
export const getGlobalCallsState = () => {
|
||||
return globalStateSubject.value;
|
||||
};
|
||||
|
||||
export const setGlobalCallsState = (globalState: GlobalCallsState) => {
|
||||
globalStateSubject.next(globalState);
|
||||
};
|
||||
|
||||
export const observeGlobalCallsState = () => {
|
||||
return globalStateSubject.asObservable();
|
||||
};
|
||||
|
||||
export const useGlobalCallsState = () => {
|
||||
const [state, setState] = useState<GlobalCallsState>(DefaultGlobalCallsState);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = globalStateSubject.subscribe((globalState) => {
|
||||
setState(globalState);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription?.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
|
|
@ -6,3 +6,4 @@ export * from './calls_state';
|
|||
export * from './calls_config';
|
||||
export * from './current_call';
|
||||
export * from './channels_with_calls';
|
||||
export * from './global_calls_state';
|
||||
|
|
|
|||
|
|
@ -4,6 +4,14 @@
|
|||
import type UserModel from '@typings/database/models/servers/user';
|
||||
import type {ConfigurationParamWithUrls, ConfigurationParamWithUrl} from 'react-native-webrtc';
|
||||
|
||||
export type GlobalCallsState = {
|
||||
micPermissionsGranted: boolean;
|
||||
}
|
||||
|
||||
export const DefaultGlobalCallsState: GlobalCallsState = {
|
||||
micPermissionsGranted: false,
|
||||
};
|
||||
|
||||
export type CallsState = {
|
||||
serverUrl: string;
|
||||
myUserId: string;
|
||||
|
|
@ -11,12 +19,12 @@ export type CallsState = {
|
|||
enabled: Dictionary<boolean>;
|
||||
}
|
||||
|
||||
export const DefaultCallsState = {
|
||||
export const DefaultCallsState: CallsState = {
|
||||
serverUrl: '',
|
||||
myUserId: '',
|
||||
calls: {} as Dictionary<Call>,
|
||||
enabled: {} as Dictionary<boolean>,
|
||||
} as CallsState;
|
||||
};
|
||||
|
||||
export type Call = {
|
||||
participants: Dictionary<CallParticipant>;
|
||||
|
|
@ -27,26 +35,41 @@ export type Call = {
|
|||
ownerId: string;
|
||||
}
|
||||
|
||||
export const DefaultCall = {
|
||||
export const DefaultCall: Call = {
|
||||
participants: {} as Dictionary<CallParticipant>,
|
||||
channelId: '',
|
||||
startTime: 0,
|
||||
screenOn: '',
|
||||
threadId: '',
|
||||
ownerId: '',
|
||||
};
|
||||
|
||||
export type CurrentCall = {
|
||||
export type CurrentCall = Call & {
|
||||
connected: boolean;
|
||||
serverUrl: string;
|
||||
myUserId: string;
|
||||
participants: Dictionary<CallParticipant>;
|
||||
channelId: string;
|
||||
startTime: number;
|
||||
screenOn: string;
|
||||
threadId: string;
|
||||
screenShareURL: string;
|
||||
speakerphoneOn: boolean;
|
||||
voiceOn: Dictionary<boolean>;
|
||||
micPermissionsErrorDismissed: boolean;
|
||||
}
|
||||
|
||||
export const DefaultCurrentCall: CurrentCall = {
|
||||
connected: false,
|
||||
serverUrl: '',
|
||||
myUserId: '',
|
||||
participants: {},
|
||||
channelId: '',
|
||||
startTime: 0,
|
||||
screenOn: '',
|
||||
threadId: '',
|
||||
ownerId: '',
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
voiceOn: {},
|
||||
micPermissionsErrorDismissed: false,
|
||||
};
|
||||
|
||||
export type CallParticipant = {
|
||||
id: string;
|
||||
muted: boolean;
|
||||
|
|
@ -89,6 +112,7 @@ export type CallsConnection = {
|
|||
waitForPeerConnection: () => Promise<void>;
|
||||
raiseHand: () => void;
|
||||
unraiseHand: () => void;
|
||||
initializeVoiceTrack: () => void;
|
||||
}
|
||||
|
||||
export type ServerCallsConfig = {
|
||||
|
|
@ -106,7 +130,7 @@ export type CallsConfig = ServerCallsConfig & {
|
|||
last_retrieved_at: number;
|
||||
}
|
||||
|
||||
export const DefaultCallsConfig = {
|
||||
export const DefaultCallsConfig: CallsConfig = {
|
||||
pluginEnabled: false,
|
||||
ICEServers: [], // deprecated
|
||||
ICEServersConfigs: [],
|
||||
|
|
@ -116,7 +140,7 @@ export const DefaultCallsConfig = {
|
|||
last_retrieved_at: 0,
|
||||
sku_short_name: '',
|
||||
MaxCallParticipants: 0,
|
||||
} as CallsConfig;
|
||||
};
|
||||
|
||||
export type ICEServersConfigs = Array<ConfigurationParamWithUrls | ConfigurationParamWithUrl>;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,18 +2,19 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database, Q} from '@nozbe/watermelondb';
|
||||
import {of as of$, Observable} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
import {of as of$, Observable, combineLatest} from 'rxjs';
|
||||
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
||||
|
||||
import {Config, Preferences} from '@constants';
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {PUSH_PROXY_STATUS_UNKNOWN} from '@constants/push_proxy';
|
||||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type ConfigModel from '@typings/database/models/servers/config';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
|
||||
export type PrepareCommonSystemValuesArgs = {
|
||||
config?: ClientConfig;
|
||||
lastUnreadChannelId?: string;
|
||||
currentChannelId?: string;
|
||||
currentTeamId?: string;
|
||||
|
|
@ -22,7 +23,7 @@ export type PrepareCommonSystemValuesArgs = {
|
|||
teamHistory?: string;
|
||||
}
|
||||
|
||||
const {SERVER: {SYSTEM}} = MM_TABLES;
|
||||
const {SERVER: {SYSTEM, CONFIG}} = MM_TABLES;
|
||||
|
||||
export const getCurrentChannelId = async (serverDatabase: Database): Promise<string> => {
|
||||
try {
|
||||
|
|
@ -94,7 +95,6 @@ export const observePushVerificationStatus = (database: Database): Observable<st
|
|||
|
||||
export const getCommonSystemValues = async (serverDatabase: Database) => {
|
||||
const systemRecords = (await serverDatabase.collections.get<SystemModel>(SYSTEM).query().fetch());
|
||||
let config: ClientConfig = {} as ClientConfig;
|
||||
let license: ClientLicense = {} as ClientLicense;
|
||||
let currentChannelId = '';
|
||||
let currentTeamId = '';
|
||||
|
|
@ -102,9 +102,6 @@ export const getCommonSystemValues = async (serverDatabase: Database) => {
|
|||
let lastUnreadChannelId = '';
|
||||
systemRecords.forEach((systemRecord) => {
|
||||
switch (systemRecord.id) {
|
||||
case SYSTEM_IDENTIFIERS.CONFIG:
|
||||
config = systemRecord.value;
|
||||
break;
|
||||
case SYSTEM_IDENTIFIERS.CURRENT_CHANNEL_ID:
|
||||
currentChannelId = systemRecord.value;
|
||||
break;
|
||||
|
|
@ -128,51 +125,75 @@ export const getCommonSystemValues = async (serverDatabase: Database) => {
|
|||
currentTeamId,
|
||||
currentUserId,
|
||||
lastUnreadChannelId,
|
||||
config,
|
||||
license,
|
||||
};
|
||||
};
|
||||
|
||||
export const getConfig = async (serverDatabase: Database) => {
|
||||
try {
|
||||
const config = await serverDatabase.get<SystemModel>(SYSTEM).find(SYSTEM_IDENTIFIERS.CONFIG);
|
||||
return (config?.value || {}) as ClientConfig;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
const fromModelToClientConfig = (list: ConfigModel[]) => {
|
||||
const config: {[key: string]: any} = {};
|
||||
list.forEach((v) => {
|
||||
config[v.id] = v.value;
|
||||
});
|
||||
return config as ClientConfig;
|
||||
};
|
||||
|
||||
export const getConfig = async (database: Database) => {
|
||||
const configList = await database.get<ConfigModel>(CONFIG).query().fetch();
|
||||
return fromModelToClientConfig(configList);
|
||||
};
|
||||
|
||||
export const queryConfigValue = (database: Database, key: keyof ClientConfig) => {
|
||||
return database.get<ConfigModel>(CONFIG).query(Q.where('id', Q.eq(key)));
|
||||
};
|
||||
|
||||
export const getConfigValue = async (database: Database, key: keyof ClientConfig) => {
|
||||
const list = await queryConfigValue(database, key).fetch();
|
||||
return list.length ? list[0].value : undefined;
|
||||
};
|
||||
|
||||
export const observeConfig = (database: Database): Observable<ClientConfig | undefined> => {
|
||||
return querySystemValue(database, SYSTEM_IDENTIFIERS.CONFIG).observe().pipe(
|
||||
switchMap((result) => (result.length ? result[0].observe() : of$({value: undefined}))),
|
||||
switchMap((model) => of$(model.value)),
|
||||
return database.get<ConfigModel>(CONFIG).query().observeWithColumns(['value']).pipe(
|
||||
switchMap((result) => of$(fromModelToClientConfig(result))),
|
||||
);
|
||||
};
|
||||
|
||||
export const observeConfigValue = (database: Database, key: keyof ClientConfig) => {
|
||||
return observeConfig(database).pipe(
|
||||
switchMap((cfg) => of$(cfg?.[key])),
|
||||
return queryConfigValue(database, key).observeWithColumns(['value']).pipe(
|
||||
switchMap((result) => of$(result.length ? result[0].value : undefined)),
|
||||
);
|
||||
};
|
||||
|
||||
export const observeMaxFileCount = (database: Database) => {
|
||||
return observeConfigValue(database, 'Version').pipe(
|
||||
switchMap((v) => of$(isMinimumServerVersion(v || '', 6, 0) ? 10 : 5)),
|
||||
);
|
||||
};
|
||||
|
||||
export const observeIsCustomStatusExpirySupported = (database: Database) => {
|
||||
return observeConfigValue(database, 'Version').pipe(
|
||||
switchMap((v) => of$(isMinimumServerVersion(v || '', 5, 37))),
|
||||
);
|
||||
};
|
||||
|
||||
export const observeConfigBooleanValue = (database: Database, key: keyof ClientConfig) => {
|
||||
return observeConfig(database).pipe(
|
||||
switchMap((cfg) => of$(cfg?.[key] === 'true')),
|
||||
return observeConfigValue(database, key).pipe(
|
||||
switchMap((v) => of$(v === 'true')),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
};
|
||||
|
||||
export const observeConfigIntValue = (database: Database, key: keyof ClientConfig, defaultValue = 0) => {
|
||||
return observeConfig(database).pipe(
|
||||
switchMap((cfg) => of$((parseInt(cfg?.[key] || '0', 10) || defaultValue))),
|
||||
return observeConfigValue(database, key).pipe(
|
||||
switchMap((v) => of$((parseInt(v || '0', 10) || defaultValue))),
|
||||
);
|
||||
};
|
||||
|
||||
export const observeIsPostPriorityEnabled = (database: Database) => {
|
||||
const config = observeConfig(database);
|
||||
return config.pipe(
|
||||
switchMap(
|
||||
(cfg) => of$(cfg?.FeatureFlagPostPriority === Config.TRUE && cfg?.PostPriority === Config.TRUE),
|
||||
),
|
||||
const featureFlag = observeConfigValue(database, 'FeatureFlagPostPriority');
|
||||
const cfg = observeConfigValue(database, 'PostPriority');
|
||||
return combineLatest([featureFlag, cfg]).pipe(
|
||||
switchMap(([ff, c]) => of$(ff === Config.TRUE && c === Config.TRUE)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -297,14 +318,8 @@ export const patchTeamHistory = (operator: ServerDataOperator, value: string[],
|
|||
export async function prepareCommonSystemValues(
|
||||
operator: ServerDataOperator, values: PrepareCommonSystemValuesArgs): Promise<SystemModel[]> {
|
||||
try {
|
||||
const {config, lastUnreadChannelId, currentChannelId, currentTeamId, currentUserId, license} = values;
|
||||
const {lastUnreadChannelId, currentChannelId, currentTeamId, currentUserId, license} = values;
|
||||
const systems: IdValue[] = [];
|
||||
if (config !== undefined) {
|
||||
systems.push({
|
||||
id: SYSTEM_IDENTIFIERS.CONFIG,
|
||||
value: JSON.stringify(config),
|
||||
});
|
||||
}
|
||||
|
||||
if (license !== undefined) {
|
||||
systems.push({
|
||||
|
|
@ -431,3 +446,17 @@ export const getExpiredSession = async (database: Database) => {
|
|||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const observeCanUploadFiles = (database: Database) => {
|
||||
const enableFileAttachments = observeConfigBooleanValue(database, 'EnableFileAttachments');
|
||||
const enableMobileFileUpload = observeConfigBooleanValue(database, 'EnableMobileFileUpload');
|
||||
const license = observeLicense(database);
|
||||
|
||||
return combineLatest([enableFileAttachments, enableMobileFileUpload, license]).pipe(
|
||||
switchMap(([efa, emfu, l]) => of$(
|
||||
efa ||
|
||||
(l?.IsLicensed !== 'true' && l?.Compliance !== 'true' && emfu),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {MM_TABLES} from '@constants/database';
|
|||
import {processIsCRTEnabled} from '@utils/thread';
|
||||
|
||||
import {queryPreferencesByCategoryAndName} from './preference';
|
||||
import {getConfig, observeConfig} from './system';
|
||||
import {getConfig, observeConfigValue} from './system';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type Model from '@nozbe/watermelondb/Model';
|
||||
|
|
@ -22,7 +22,7 @@ const {SERVER: {CHANNEL, POST, THREAD, THREADS_IN_TEAM, THREAD_PARTICIPANT, USER
|
|||
export const getIsCRTEnabled = async (database: Database): Promise<boolean> => {
|
||||
const config = await getConfig(database);
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).fetch();
|
||||
return processIsCRTEnabled(preferences, config);
|
||||
return processIsCRTEnabled(preferences, config?.CollapsedThreads, config?.FeatureFlagCollapsedThreads);
|
||||
};
|
||||
|
||||
export const getThreadById = async (database: Database, threadId: string) => {
|
||||
|
|
@ -35,11 +35,12 @@ export const getThreadById = async (database: Database, threadId: string) => {
|
|||
};
|
||||
|
||||
export const observeIsCRTEnabled = (database: Database) => {
|
||||
const config = observeConfig(database);
|
||||
const cfgValue = observeConfigValue(database, 'CollapsedThreads');
|
||||
const featureFlag = observeConfigValue(database, 'FeatureFlagCollapsedThreads');
|
||||
const preferences = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).observeWithColumns(['value']);
|
||||
return combineLatest([config, preferences]).pipe(
|
||||
return combineLatest([cfgValue, featureFlag, preferences]).pipe(
|
||||
map(
|
||||
([cfg, prefs]) => processIsCRTEnabled(prefs, cfg),
|
||||
([cfgV, ff, prefs]) => processIsCRTEnabled(prefs, cfgV, ff),
|
||||
),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {observeMyChannel} from '@queries/servers/channel';
|
|||
import {isChannelAdmin} from '@utils/user';
|
||||
|
||||
import {queryPreferencesByCategoryAndName} from './preference';
|
||||
import {observeConfig, observeCurrentUserId, observeLicense, getCurrentUserId, getConfig, getLicense} from './system';
|
||||
import {observeCurrentUserId, observeLicense, getCurrentUserId, getConfig, getLicense, observeConfigValue} from './system';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership';
|
||||
|
|
@ -67,13 +67,14 @@ export async function prepareUsers(operator: ServerDataOperator, users: UserProf
|
|||
}
|
||||
|
||||
export const observeTeammateNameDisplay = (database: Database) => {
|
||||
const config = observeConfig(database);
|
||||
const lockTeammateNameDisplay = observeConfigValue(database, 'LockTeammateNameDisplay');
|
||||
const teammateNameDisplay = observeConfigValue(database, 'TeammateNameDisplay');
|
||||
const license = observeLicense(database);
|
||||
const preferences = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).
|
||||
observeWithColumns(['value']);
|
||||
return combineLatest([config, license, preferences]).pipe(
|
||||
return combineLatest([lockTeammateNameDisplay, teammateNameDisplay, license, preferences]).pipe(
|
||||
switchMap(
|
||||
([cfg, lcs, prefs]) => of$(getTeammateNameDisplaySetting(prefs, cfg, lcs)),
|
||||
([ltnd, tnd, lcs, prefs]) => of$(getTeammateNameDisplaySetting(prefs, ltnd, tnd, lcs)),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
|
@ -82,7 +83,7 @@ export async function getTeammateNameDisplay(database: Database) {
|
|||
const config = await getConfig(database);
|
||||
const license = await getLicense(database);
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).fetch();
|
||||
return getTeammateNameDisplaySetting(preferences, config, license);
|
||||
return getTeammateNameDisplaySetting(preferences, config?.LockTeammateNameDisplay, config?.TeammateNameDisplay, license);
|
||||
}
|
||||
|
||||
export const queryUsersLike = (database: Database, likeUsername: string) => {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {switchMap} from 'rxjs/operators';
|
|||
import {Permissions} from '@constants';
|
||||
import {queryAllMyChannel} from '@queries/servers/channel';
|
||||
import {queryRolesByNames} from '@queries/servers/role';
|
||||
import {observeConfig, observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeConfigBooleanValue, observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeUser} from '@queries/servers/user';
|
||||
import {hasPermission} from '@utils/role';
|
||||
|
||||
|
|
@ -18,15 +18,8 @@ import SearchHandler from './search_handler';
|
|||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const config = observeConfig(database);
|
||||
|
||||
const sharedChannelsEnabled = config.pipe(
|
||||
switchMap((v) => of$(v?.ExperimentalSharedChannels === 'true')),
|
||||
);
|
||||
|
||||
const canShowArchivedChannels = config.pipe(
|
||||
switchMap((v) => of$(v?.ExperimentalViewArchivedChannels === 'true')),
|
||||
);
|
||||
const sharedChannelsEnabled = observeConfigBooleanValue(database, 'ExperimentalSharedChannels');
|
||||
const canShowArchivedChannels = observeConfigBooleanValue(database, 'ExperimentalViewArchivedChannels');
|
||||
|
||||
const currentTeamId = observeCurrentTeamId(database);
|
||||
const currentUserId = observeCurrentUserId(database);
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -234,8 +234,6 @@ const PublicChannelIllustration = ({theme}: Props) => (
|
|||
id='image0_516_33521'
|
||||
width={72}
|
||||
height={77}
|
||||
|
||||
// @ts-expect-error string source
|
||||
xlinkHref='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABNCAYAAAAFICL0AAALM0lEQVR4XtWce68URRDFq8Uo8YHGBz5QfBCNGvnH7/8tNBolIgqIGlExgmgwbX6Vqk51T/fM7GXv3tmbkMvd2Z2drqk6darq9CTZw0/O+bWU0s+jU+WcnxSRJ1NKD+e+Luf8gojcTyk92sNl7eUU6XHOYgt6lFK6z3lyzk+nlP5Zc06MtiVDjK55ZwNhlJTSvd4Jewby13LOV1JK3/K3iPwnIpznrhl27px4H4bXm3Don1UGshB5eS6M/MI93DxcRKT6XM6Zv9Uwx/CzykALuFGFlYdOzvn8HOaY0YsXbdVYlYEiLsRQ4q6LCMD5sMWYnPOzLG4UAhyPx6IHRSM2r2/Gy1oDkUVY7L3m4s+LyDkR+ceBdRQqOec3U0o/+nE3UAy9EYYZHoE5XMMmMlnaZzaxsDnX8bJuxuImmGdilLw2Ax4yHDHQ6tRsd5hF8RnNZLZIPA+OgyHuNqH6oYgAyhjhDwPtH8Nnsx3js+q5IkJYbgLIuyBtnlC5uV043lHSbXyfg66I3CM8QmiBUQD2ZME97+2RypFnHsKTRgbiLnZd3gAb4+Epb5p3PGd/l8wF5ojIX24cS/vqYYESXE0pfW7eNAT73s05hHEUC5e+qJOFHLApL647sLrXAOR2ToyHt7lHOVFUUmjZb+JZc0R06VpP4/jEQHbhmq2M9ZJyFTPMGFdE5FcR+UBErtnL3H0w6XX7+xZs2c6BoTj2nB27a6/P1m+nsdiTnLNN8+odobYakr1ACMtC3bgi8o6IfG+GAMDxKgVee8+LKaXbay649eA1n9nne9aEmKdiLRlaYG0JnoUVBWzFY4JByWaXPTzNKzfrTYsGsgUUKtAwbBbbLRcMlPFAbYMYuHuRO1vXLbU9cs7vpJS+36enjM41ymIV1W+MUoVhPLFX7AGv8JQflrzE0vjFiHVzi98nuV0ycotBXiYsFZq0LKjPfrEvALB/E5GnROQBeGPpv9RoXroEnIrtjjNtacwZaW2IVQZzjzJ+MiwRQlhNyo/gZXNlyJmXH70sNrmoQAi10QUB9Lqpx1tyzs+IyL+Wxd4TkT8tg03AeGRkM275nqVQOK3jk3YHXxQq9pjCy53OObPom+GiNFxyzhdE5HnYM6GWUroTa72Yspf62Ke14F3POwLpqofTnjTgiVKAYNBJVmsbZ4P6azP9n3atIwNRR+FJnqLLwq3GguPwmrNiiCF1Fz+8TmOtrfY5DtOGhWvB2+FKszdm17u/j/f3QszD5ZOU0pdtZR85SiB/4FIpRGPLosUoMzDGuwTbNua+ahKyjwXveg76QaTk0inshBPe5PMs7rwbkFSP51CwkvKp0uk6au0WSxF73SlB4Tv23ZvsA7kdhh1F85yqJDDCF8Ea43qLlAr9sogQlrzmBe+kJ23eNhk0HpIArvUk7Sh65R24yYcppW9swXgGlTi/MQjegpE4rov3xr29D3zykuKi4VQVQls0xLDUmAuxECYsFMNci0370Ot5iTYsbBp+ZIZNa+ulLRtMQdqb5zFdxwzjk4rgYT4GKnN0C0k8yvvKbXsWLNOsF+9WmLRuZpIRr69ksZmZGFwHD+EHJu2pHZzRcLNM9INnPPOmV6IHdTqT3lmshgDhJmxidh8NNOxDN3e8ynqNYfESJhd4g+JObwDQgn0v/nPOioNrwfS03jdbrFoNxkK1JjIO40TQhQd6p5uSwr0DfkRIcQ6v2LvCh9Na4OOet/KgJf1OhyN5+MFxtGKnJyQiP1l220SYPI6RZsc+zmcCOSycJuANGQ4CWY10LIzck6oaLSg/dGx0VtKWNYbrTTUms/HOpJR6isqdtP7nltP0GiPMvac79pm7oz5qtga+i6H47cy525LNOb+UUqLreFQ/EYOKyitOMoNKY4InUfrSFLGlqW7ADqC/JSJ3zDowc9okBbDtXJe2kLm6PGiQajX1G//xgtR5DyWGZzAvaDXDGR6NZHpHBdyRScNvAGGdQhjIKmOOeqHQj45ED7ClB+SqjVjQfiYiX3tWMxI57B5sLf5WpfmoZh0INaNBkLvQ5/HR80Rec5ZqjV1vwJz8pRoINuEEcSTEVNfsQk37O14DzXvC9OaSYmxpWLjrwvb1/l5HsZLc+oTUWhhFhmcs27VCOkHtzbwG2KZ13JJqNuf8fhxR72vRu5ynZ6Ayw4rzdCsnYMmED+QQEKZ41ao95/xxSumrBr9URxReq8Y+W5O69AzXnayGBXGnMQb8Rb3Hjjn/AdhLurY+EHqgSko3EkAdA8GcqFzDNAKwxSB4yPWQvWi2w6QxHviiAinLehGswacLKaWbjVRv0sHcxeUP/V5P86VmCottBeI6t28v0CagvmhwCGrAZ6NQU2dnI031WWuAdio1Yng17VWAeJXoqcEhuo5Lu3y6xj+0t6zBoGrUbLhTNb+C0XQWFk7alg6TaWmv/x1btVswSHsNi+oOWxQq1pjGXW1WJL5hAgJGqbgzGHNzE9O1NyMyabCD8TD6wWpmZVkInIH38BtQ5rP8roRPYW4/krVEtdpOIva1i9rn+6KBNLxGjNaMxHfTwP/FQJcQ0+Gh66RdX22hQ432s3UaY7dgU/sxTgTSnrZF5F0RudGEjHYI8TYRuRX3WLR1Vi9DubBqqfzYpyec9FzV2CeGS8NdyiTDhU3GovV7g6YaZj2cb7XztZNe9CE/NypWwRnSOoy4wpJ2yBhSOp+hEUYJQtp28abWU1HguWXeM8liAwmd7t4x7Hjb2PSN4D0fWdjBbwB3PAcD8YOB4sR10oIl48W+k4XzJkVUzqRZ8Hdh2BenFzHreO3FeGey+aTjbdU277n9qlvdyzopVm1SoczXcOiqiFClA8hMTVXk6U00GwmhB8JgKooKhu5J9Dj1UPV6SHxZ813uQT1vIHRI/ZXKLIgNXBukbdqcM4b0UTHGLBvs2gtZkg+vufBDvccN1CrrwROOaUgZQeT/EEWqfAglfR6qfcKIpv11ai5XjZmBtO26tJgtgzYCKt2NY4tzWQuhgRFehee0IWEy4CfsQQEuonImDmDz/z8aAcPmt4D3bqRK8Dr8R8sJwxomqEjr7sQnJTSZqmwlCC3a1kBtK3fpeR9n3m5VTmdg3N3bHprwLvsFXHU6aqHE66V2yzm7Mta3UKmgKt6ZY2izxuvFg1wEVTSIo0Z5u42y3aHczPCrxry3XW0cNNsfWsKsQx7vijjNQ8AmKvVroZqnUAWoX4EoppQesP0AAUPjJVqaLD2U5Ch60iHr+BMX8CjCwvWG3hgrbdRQXuAlAPnvsYrvKeh7WmzGOiJye4sPFPA1lp50KC08tbNtAGIIzpDaqa18BzMYAzXQFmxohfDeuA9sUw8JOEloutI+qt0Z7+AZ/NN+chBLuTfxHn44TkGr5UhUznfI4fAZQSe58EN9ZsKkgzhcZbtxrOzVvYdfKEeURMYWaxtSYQg5u/XhUAtf+z1uIO0MmgE+TSl9EVL85Jli7R4wA21mZVFEBcAXpcjaC9ra+5woVsWjiZ7AnzdSSrQ5fO87oeUFaMtvXCukID+3h2xrRpi7Hscgwsnvtg/+ok4ID0OpQbX+N0brYAz9Z+1Vd44dlWiqIoqGI97s0papNb98WgoIe+rvKjJ6ou9eU+yYPKek+ZmHjET5HcqNz8N8HuNBFplYMFIeeojJZLqedQwGKw9Y6nX0mvSNwWjDInXxJ8CQummWud55szsHT3ozqn6QgTHhhGfoPrD4u2HQOz3b8NiK1BhirWTFSw7nQTr8Y0Nuu//LwlO9KBivSwh3fRTYSe/4vj8X52K+m7CahIYHselYJ46ljWG3WsZqu8KajuK+F7XP8/0Pwx3exhMOW2YAAAAASUVORK5CYII='
|
||||
/>
|
||||
</Defs>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -6,11 +6,7 @@ import withObservables from '@nozbe/with-observables';
|
|||
import {combineLatest, of as of$} from 'rxjs';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {observeConfig, observeLicense} from '@queries/servers/system';
|
||||
import {observeCurrentUser, observeUser} from '@queries/servers/user';
|
||||
import {observeCurrentUser, observeTeammateNameDisplay, observeUser} from '@queries/servers/user';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
import PublicOrPrivateChannel from './public_or_private_channel';
|
||||
|
|
@ -21,16 +17,10 @@ import type ChannelModel from '@typings/database/models/servers/channel';
|
|||
const enhanced = withObservables([], ({channel, database}: {channel: ChannelModel} & WithDatabaseArgs) => {
|
||||
let creator;
|
||||
if (channel.creatorId) {
|
||||
const config = observeConfig(database);
|
||||
const license = observeLicense(database);
|
||||
const preferences = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).
|
||||
observeWithColumns(['value']);
|
||||
const me = observeCurrentUser(database);
|
||||
const profile = observeUser(database, channel.creatorId);
|
||||
|
||||
const teammateNameDisplay = combineLatest([preferences, config, license]).pipe(
|
||||
map(([prefs, cfg, lcs]) => getTeammateNameDisplaySetting(prefs, cfg, lcs)),
|
||||
);
|
||||
const teammateNameDisplay = observeTeammateNameDisplay(database);
|
||||
|
||||
creator = combineLatest([profile, teammateNameDisplay, me]).pipe(
|
||||
map(([user, displaySetting, currentUser]) => (user ? displayUsername(user, currentUser?.locale, displaySetting, true) : '')),
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const enhanced = withObservables([], ({database, serverUrl}: EnhanceProps) => {
|
|||
distinctUntilChanged(),
|
||||
);
|
||||
const isInACall = currentCall.pipe(
|
||||
switchMap((call) => of$(Boolean(call))),
|
||||
switchMap((call) => of$(Boolean(call?.connected))),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const isInCurrentChannelCall = combineLatest([channelId, ccChannelId]).pipe(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {switchMap} from 'rxjs/operators';
|
|||
|
||||
import {General} from '@constants';
|
||||
import {observeProfileLongPresTutorial} from '@queries/app/global';
|
||||
import {observeConfig, observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeConfigValue, observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeTeammateNameDisplay} from '@queries/servers/user';
|
||||
|
||||
import CreateDirectMessage from './create_direct_message';
|
||||
|
|
@ -16,8 +16,8 @@ import CreateDirectMessage from './create_direct_message';
|
|||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const restrictDirectMessage = observeConfig(database).pipe(
|
||||
switchMap((cfg) => of$(cfg?.RestrictDirectMessage !== General.RESTRICT_DIRECT_MESSAGE_ANY)),
|
||||
const restrictDirectMessage = observeConfigValue(database, 'RestrictDirectMessage').pipe(
|
||||
switchMap((v) => of$(v !== General.RESTRICT_DIRECT_MESSAGE_ANY)),
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
// 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 {observeIsCustomStatusExpirySupported} from '@queries/servers/system';
|
||||
|
||||
import CustomStatusSuggestion from './custom_status_suggestion';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
return {
|
||||
isExpirySupported: observeIsCustomStatusExpirySupported(database),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(CustomStatusSuggestion));
|
||||
|
|
@ -13,7 +13,6 @@ import CustomStatusSuggestion from './custom_status_suggestion';
|
|||
|
||||
type Props = {
|
||||
intl: IntlShape;
|
||||
isExpirySupported: boolean;
|
||||
onHandleCustomStatusSuggestionClick: (status: UserCustomStatus) => void;
|
||||
recentCustomStatuses: UserCustomStatus[];
|
||||
theme: Theme;
|
||||
|
|
@ -57,7 +56,6 @@ const defaultCustomStatusSuggestions: DefaultUserCustomStatus[] = [
|
|||
|
||||
const CustomStatusSuggestions = ({
|
||||
intl,
|
||||
isExpirySupported,
|
||||
onHandleCustomStatusSuggestionClick,
|
||||
recentCustomStatuses,
|
||||
theme,
|
||||
|
|
@ -81,7 +79,6 @@ const CustomStatusSuggestions = ({
|
|||
theme={theme}
|
||||
separator={index !== arr.length - 1}
|
||||
duration={status.duration}
|
||||
isExpirySupported={isExpirySupported}
|
||||
/>
|
||||
));
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import CustomStatusSuggestion from '@screens/custom_status/components/custom_sta
|
|||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
isExpirySupported: boolean;
|
||||
onHandleClear: (status: UserCustomStatus) => void;
|
||||
onHandleSuggestionClick: (status: UserCustomStatus) => void;
|
||||
recentCustomStatuses: UserCustomStatus[];
|
||||
|
|
@ -38,7 +37,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
const RecentCustomStatuses = ({isExpirySupported, onHandleClear, onHandleSuggestionClick, recentCustomStatuses, theme}: Props) => {
|
||||
const RecentCustomStatuses = ({onHandleClear, onHandleSuggestionClick, recentCustomStatuses, theme}: Props) => {
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
if (recentCustomStatuses.length === 0) {
|
||||
|
|
@ -66,7 +65,6 @@ const RecentCustomStatuses = ({isExpirySupported, onHandleClear, onHandleSuggest
|
|||
separator={index !== recentCustomStatuses.length - 1}
|
||||
duration={status.duration}
|
||||
expires_at={status.expires_at}
|
||||
isExpirySupported={isExpirySupported}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ import {injectIntl, IntlShape} from 'react-intl';
|
|||
import {BackHandler, DeviceEventEmitter, Keyboard, KeyboardAvoidingView, Platform, ScrollView, View} from 'react-native';
|
||||
import {EventSubscription, Navigation, NavigationButtonPressedEvent, NavigationComponent, NavigationComponentProps} from 'react-native-navigation';
|
||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {updateLocalCustomStatus} from '@actions/local/user';
|
||||
import {removeRecentCustomStatus, updateCustomStatus, unsetCustomStatus} from '@actions/remote/user';
|
||||
|
|
@ -20,11 +18,11 @@ import {Events, Screens} from '@constants';
|
|||
import {CustomStatusDurationEnum, SET_CUSTOM_STATUS_FAILURE} from '@constants/custom_status';
|
||||
import {withServerUrl} from '@context/server';
|
||||
import {withTheme} from '@context/theme';
|
||||
import {observeConfig, observeRecentCustomStatus} from '@queries/servers/system';
|
||||
import {observeIsCustomStatusExpirySupported, observeRecentCustomStatus} from '@queries/servers/system';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
import {dismissModal, goToScreen, showModal} from '@screens/navigation';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {getCurrentMomentForTimezone, getRoundedTime, isCustomStatusExpirySupported} from '@utils/helpers';
|
||||
import {getCurrentMomentForTimezone, getRoundedTime} from '@utils/helpers';
|
||||
import {logDebug} from '@utils/log';
|
||||
import {mergeNavigationOptions} from '@utils/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
|
@ -376,7 +374,6 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
|||
</View>
|
||||
{recentCustomStatuses.length > 0 && (
|
||||
<RecentCustomStatuses
|
||||
isExpirySupported={customStatusExpirySupported}
|
||||
onHandleClear={this.handleRecentCustomStatusClear}
|
||||
onHandleSuggestionClick={this.handleRecentCustomStatusSuggestionClick}
|
||||
recentCustomStatuses={recentCustomStatuses}
|
||||
|
|
@ -386,7 +383,6 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
|||
}
|
||||
<CustomStatusSuggestions
|
||||
intl={intl}
|
||||
isExpirySupported={customStatusExpirySupported}
|
||||
onHandleCustomStatusSuggestionClick={this.handleCustomStatusSuggestionClick}
|
||||
recentCustomStatuses={recentCustomStatuses}
|
||||
theme={theme}
|
||||
|
|
@ -404,13 +400,10 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
|||
const augmentCSM = injectIntl(withTheme(withServerUrl(CustomStatusModal)));
|
||||
|
||||
const enhancedCSM = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const config = observeConfig(database);
|
||||
return {
|
||||
currentUser: observeCurrentUser(database),
|
||||
customStatusExpirySupported: config.pipe(
|
||||
switchMap((cfg) => of$(isCustomStatusExpirySupported(cfg?.Version || ''))),
|
||||
),
|
||||
recentCustomStatuses: observeRecentCustomStatus(database),
|
||||
customStatusExpirySupported: observeIsCustomStatusExpirySupported(database),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue