forked from Ivasoft/mattermost-mobile
Graph QL POC (#6024)
* First approach * Lint * Fixes and adding monitoring console statements (to be removed later) * Add pagination and apply graphQL also to login * Get all entry points to use the same GQL call * Unify gql handling * Use graphQL on websocket reconnect * Handle latest changes regarding categories * Use graphQL to properly fetch channel members on other servers * Remove logs and fetch unreads from other teams * Minor fixes * Final fixes * Address feedback, minor refactoring, and fixes around the refactor * Fix custom status duration types * Add missing fields and some reordering * Add timeout to fetch posts for unread channels
This commit is contained in:
committed by
GitHub
parent
6c5043d598
commit
bae5477b35
@@ -37,8 +37,7 @@ export const searchGroupsByNameInTeam = async (serverUrl: string, name: string,
|
||||
try {
|
||||
database = DatabaseManager.getServerDatabaseAndOperator(serverUrl).database;
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('searchGroupsByNameInTeam - DB Error', e);
|
||||
logError('searchGroupsByNameInTeam - DB Error', e);
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
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, getCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId, getConfig, getCurrentChannelId} from '@queries/servers/system';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {deleteV1Data} from '@utils/file';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logInfo} from '@utils/log';
|
||||
import {logDebug, logInfo} from '@utils/log';
|
||||
|
||||
import {deferredAppEntryActions, entry, registerDeviceToken, syncOtherServers, verifyPushProxy} from './common';
|
||||
import {graphQLCommon} from './gql_common';
|
||||
|
||||
export async function appEntry(serverUrl: string, since = 0, isUpgrade = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
@@ -30,6 +31,33 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
|
||||
operator.batchRecords(removeLastUnreadChannelId);
|
||||
}
|
||||
|
||||
const config = await getConfig(database);
|
||||
let result;
|
||||
if (config?.FeatureFlagGraphQL === 'true') {
|
||||
const {currentTeamId, currentChannelId} = await getCommonSystemValues(database);
|
||||
result = await graphQLCommon(serverUrl, true, currentTeamId, currentChannelId, isUpgrade);
|
||||
if (result.error) {
|
||||
logDebug('Error using GraphQL, trying REST', result.error);
|
||||
result = restAppEntry(serverUrl, since, isUpgrade);
|
||||
}
|
||||
} else {
|
||||
result = restAppEntry(serverUrl, since, isUpgrade);
|
||||
}
|
||||
|
||||
if (!since) {
|
||||
// Load data from other servers
|
||||
syncOtherServers(serverUrl);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function restAppEntry(serverUrl: string, since = 0, isUpgrade = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
const {database} = operator;
|
||||
|
||||
const tabletDevice = await isTablet();
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const currentChannelId = await getCurrentChannelId(database);
|
||||
@@ -39,6 +67,7 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
|
||||
if ('error' in entryData) {
|
||||
return {error: entryData.error};
|
||||
}
|
||||
|
||||
const {models, initialTeamId, initialChannelId, prefData, teamData, chData, meData} = entryData;
|
||||
if (isUpgrade && meData?.user) {
|
||||
const me = await prepareCommonSystemValues(operator, {currentUserId: meData.user.id});
|
||||
@@ -65,10 +94,6 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
|
||||
const {id: currentUserId, locale: currentUserLocale} = meData?.user || (await getCurrentUser(database))!;
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchToChannel ? initialChannelId : undefined);
|
||||
if (!since) {
|
||||
// Load data from other servers
|
||||
syncOtherServers(serverUrl);
|
||||
}
|
||||
|
||||
verifyPushProxy(serverUrl);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import {fetchAllTeams, fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, MyTeamsRequest} from '@actions/remote/team';
|
||||
import {fetchNewThreads} from '@actions/remote/thread';
|
||||
import {fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {gqlAllChannels} from '@client/graphQL/entry';
|
||||
import {Preferences} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {PUSH_PROXY_RESPONSE_NOT_AVAILABLE, PUSH_PROXY_RESPONSE_UNKNOWN, PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_UNKNOWN, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
|
||||
@@ -21,12 +22,14 @@ import {DEFAULT_LOCALE} from '@i18n';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getDeviceToken} from '@queries/app/global';
|
||||
import {queryAllServers} from '@queries/app/servers';
|
||||
import {queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel';
|
||||
import {prepareMyChannelsForTeam, queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel';
|
||||
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
|
||||
import {getHasCRTChanged} from '@queries/servers/preference';
|
||||
import {getConfig, getPushVerificationStatus, getWebSocketLastDisconnected} from '@queries/servers/system';
|
||||
import {getConfig, getCurrentUserId, getPushVerificationStatus, getWebSocketLastDisconnected} from '@queries/servers/system';
|
||||
import {deleteMyTeams, getAvailableTeamIds, getNthLastChannelFromTeam, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team';
|
||||
import {isDMorGM} from '@utils/channel';
|
||||
import {getMemberChannelsFromGQLQuery, gqlToClientChannelMembership} from '@utils/graphql';
|
||||
import {logDebug} from '@utils/log';
|
||||
import {processIsCRTEnabled} from '@utils/thread';
|
||||
|
||||
import {fetchGroupsForMember} from '../groups';
|
||||
@@ -61,12 +64,12 @@ export type EntryResponse = {
|
||||
}
|
||||
|
||||
const FETCH_MISSING_DM_TIMEOUT = 2500;
|
||||
const FETCH_UNREADS_TIMEOUT = 2500;
|
||||
export const FETCH_UNREADS_TIMEOUT = 2500;
|
||||
|
||||
export const teamsToRemove = async (serverUrl: string, removeTeamIds?: string[]) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return undefined;
|
||||
return [];
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
@@ -81,7 +84,7 @@ export const teamsToRemove = async (serverUrl: string, removeTeamIds?: string[])
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return [];
|
||||
};
|
||||
|
||||
export const entry = async (serverUrl: string, teamId?: string, channelId?: string, since = 0): Promise<EntryResponse> => {
|
||||
@@ -360,6 +363,51 @@ const syncAllChannelMembersAndThreads = async (serverUrl: string) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = await getConfig(database);
|
||||
|
||||
if (config?.FeatureFlagGraphQL === 'true') {
|
||||
const error = await graphQLSyncAllChannelMembers(serverUrl);
|
||||
if (error) {
|
||||
logDebug('failed graphQL, falling back to rest', error);
|
||||
restSyncAllChannelMembers(serverUrl);
|
||||
}
|
||||
} else {
|
||||
restSyncAllChannelMembers(serverUrl);
|
||||
}
|
||||
};
|
||||
|
||||
const graphQLSyncAllChannelMembers = async (serverUrl: string) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return 'Server database not found';
|
||||
}
|
||||
|
||||
const response = await gqlAllChannels(serverUrl);
|
||||
if ('error' in response) {
|
||||
return response.error;
|
||||
}
|
||||
|
||||
if (response.errors) {
|
||||
return response.errors[0].message;
|
||||
}
|
||||
|
||||
const userId = await getCurrentUserId(operator.database);
|
||||
|
||||
const channels = getMemberChannelsFromGQLQuery(response.data);
|
||||
const memberships = response.data.channelMembers?.map((m) => gqlToClientChannelMembership(m, userId));
|
||||
|
||||
if (channels && memberships) {
|
||||
const modelPromises = await prepareMyChannelsForTeam(operator, '', channels, memberships, undefined, true);
|
||||
const models = (await Promise.all(modelPromises)).flat();
|
||||
if (models.length) {
|
||||
operator.batchRecords(models);
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const restSyncAllChannelMembers = async (serverUrl: string) => {
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
|
||||
311
app/actions/remote/entry/gql_common.ts
Normal file
311
app/actions/remote/entry/gql_common.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database} from '@nozbe/watermelondb';
|
||||
|
||||
import {markChannelAsRead, MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {fetchGroupsForMember} from '@actions/remote/groups';
|
||||
import {fetchPostsForChannel, fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||
import {MyTeamsRequest} from '@actions/remote/team';
|
||||
import {fetchNewThreads} from '@actions/remote/thread';
|
||||
import {MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {gqlEntry, gqlEntryChannels, gqlOtherChannels} from '@client/graphQL/entry';
|
||||
import {Preferences} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPreferenceValue} from '@helpers/api/preference';
|
||||
import {selectDefaultTeam} from '@helpers/api/team';
|
||||
import {queryAllChannels, queryAllChannelsForTeam} from '@queries/servers/channel';
|
||||
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
|
||||
import {getHasCRTChanged} from '@queries/servers/preference';
|
||||
import {prepareCommonSystemValues} from '@queries/servers/system';
|
||||
import {addChannelToTeamHistory, addTeamToTeamHistory, queryMyTeams} from '@queries/servers/team';
|
||||
import {selectDefaultChannelForTeam} from '@utils/channel';
|
||||
import {filterAndTransformRoles, getMemberChannelsFromGQLQuery, getMemberTeamsFromGQLQuery, gqlToClientChannelMembership, gqlToClientPreference, gqlToClientSidebarCategory, gqlToClientTeamMembership, gqlToClientUser} from '@utils/graphql';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {processIsCRTEnabled} from '@utils/thread';
|
||||
|
||||
import {teamsToRemove, FETCH_UNREADS_TIMEOUT} from './common';
|
||||
|
||||
import type ClientError from '@client/rest/error';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
export async function deferredAppEntryGraphQLActions(
|
||||
serverUrl: string,
|
||||
since: number,
|
||||
meData: MyUserRequest,
|
||||
teamData: MyTeamsRequest,
|
||||
chData: MyChannelsRequest | undefined,
|
||||
isTabletDevice: boolean,
|
||||
initialTeamId?: string,
|
||||
initialChannelId?: string,
|
||||
isCRTEnabled = false,
|
||||
syncDatabase?: boolean,
|
||||
) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
const {database} = operator;
|
||||
|
||||
// defer fetching posts for initial channel
|
||||
if (initialChannelId && isTabletDevice) {
|
||||
fetchPostsForChannel(serverUrl, initialChannelId);
|
||||
markChannelAsRead(serverUrl, initialChannelId);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (chData?.channels?.length && chData.memberships?.length) {
|
||||
// defer fetching posts for unread channels on initial team
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId);
|
||||
}
|
||||
}, FETCH_UNREADS_TIMEOUT);
|
||||
|
||||
if (isCRTEnabled) {
|
||||
if (initialTeamId) {
|
||||
await fetchNewThreads(serverUrl, initialTeamId, false);
|
||||
}
|
||||
|
||||
if (teamData.teams?.length) {
|
||||
for await (const team of teamData.teams) {
|
||||
if (team.id !== initialTeamId) {
|
||||
// need to await here since GM/DM threads in different teams overlap
|
||||
await fetchNewThreads(serverUrl, team.id, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (initialTeamId) {
|
||||
const result = await getChannelData(serverUrl, initialTeamId, meData.user!.id, true);
|
||||
if ('error' in result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const removeChannels = await getRemoveChannels(database, result.chData, initialTeamId, false, syncDatabase);
|
||||
|
||||
const modelPromises = await prepareModels({operator, removeChannels, chData: result.chData}, true);
|
||||
|
||||
modelPromises.push(operator.handleRole({roles: filterAndTransformRoles(result.roles), prepareRecordsOnly: true}));
|
||||
const models = (await Promise.all(modelPromises)).flat();
|
||||
operator.batchRecords(models);
|
||||
|
||||
setTimeout(() => {
|
||||
if (result.chData?.channels?.length && result.chData.memberships?.length) {
|
||||
// defer fetching posts for unread channels on other teams
|
||||
fetchPostsForUnreadChannels(serverUrl, result.chData.channels, result.chData.memberships, initialChannelId);
|
||||
}
|
||||
}, FETCH_UNREADS_TIMEOUT);
|
||||
}
|
||||
|
||||
if (meData.user?.id) {
|
||||
// Fetch groups for current user
|
||||
fetchGroupsForMember(serverUrl, meData.user?.id);
|
||||
}
|
||||
|
||||
updateAllUsersSince(serverUrl, since);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
const getRemoveChannels = async (database: Database, chData: MyChannelsRequest | undefined, initialTeamId: string, singleTeam: boolean, syncDatabase?: boolean) => {
|
||||
const removeChannels: ChannelModel[] = [];
|
||||
if (syncDatabase) {
|
||||
if (chData?.channels) {
|
||||
const fetchedChannelIds = chData.channels?.map((channel) => channel.id);
|
||||
|
||||
const query = singleTeam ? queryAllChannelsForTeam(database, initialTeamId) : queryAllChannels(database);
|
||||
const channels = await query.fetch();
|
||||
|
||||
for (const channel of channels) {
|
||||
const excludeCondition = singleTeam ? true : channel.teamId !== initialTeamId && channel.teamId !== '';
|
||||
if (excludeCondition && !fetchedChannelIds?.includes(channel.id)) {
|
||||
removeChannels.push(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return removeChannels;
|
||||
};
|
||||
|
||||
const getChannelData = async (serverUrl: string, initialTeamId: string, userId: string, exclude: boolean): Promise<{chData: MyChannelsRequest; roles: Array<Partial<GQLRole>|undefined>} | {error: unknown}> => {
|
||||
let response;
|
||||
try {
|
||||
const request = exclude ? gqlOtherChannels : gqlEntryChannels;
|
||||
response = await request(serverUrl, initialTeamId);
|
||||
} catch (error) {
|
||||
return {error: (error as ClientError).message};
|
||||
}
|
||||
|
||||
if ('error' in response) {
|
||||
return {error: response.error};
|
||||
}
|
||||
|
||||
if ('errors' in response && response.errors?.length) {
|
||||
return {error: response.errors[0].message};
|
||||
}
|
||||
|
||||
const channelsFetchedData = response.data;
|
||||
|
||||
const chData = {
|
||||
channels: getMemberChannelsFromGQLQuery(channelsFetchedData),
|
||||
memberships: channelsFetchedData.channelMembers?.map((m) => gqlToClientChannelMembership(m, userId)),
|
||||
categories: channelsFetchedData.sidebarCategories?.map((c) => gqlToClientSidebarCategory(c, '')),
|
||||
};
|
||||
const roles = channelsFetchedData.channelMembers?.map((m) => m.roles).flat() || [];
|
||||
|
||||
return {chData, roles};
|
||||
};
|
||||
|
||||
export const graphQLCommon = async (serverUrl: string, syncDatabase: boolean, currentTeamId: string, currentChannelId: string, isUpgrade = false) => {
|
||||
const dt = Date.now();
|
||||
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
const {database} = operator;
|
||||
|
||||
const isTabletDevice = await isTablet();
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await gqlEntry(serverUrl);
|
||||
} catch (error) {
|
||||
return {error: (error as ClientError).message};
|
||||
}
|
||||
|
||||
if ('error' in response) {
|
||||
return {error: response.error};
|
||||
}
|
||||
|
||||
if ('errors' in response && response.errors?.length) {
|
||||
return {error: response.errors[0].message};
|
||||
}
|
||||
|
||||
const fetchedData = response.data;
|
||||
|
||||
const config = fetchedData.config || {} as ClientConfig;
|
||||
const license = fetchedData.license || {} as ClientLicense;
|
||||
|
||||
const meData = {
|
||||
user: gqlToClientUser(fetchedData.user!),
|
||||
};
|
||||
|
||||
const teamData = {
|
||||
teams: getMemberTeamsFromGQLQuery(fetchedData),
|
||||
memberships: fetchedData.teamMembers.map((m) => gqlToClientTeamMembership(m, meData.user.id)),
|
||||
};
|
||||
|
||||
const prefData = {
|
||||
preferences: fetchedData.user?.preferences?.map(gqlToClientPreference),
|
||||
};
|
||||
|
||||
if (prefData.preferences) {
|
||||
const crtToggled = await getHasCRTChanged(database, prefData.preferences);
|
||||
if (crtToggled) {
|
||||
const {error} = await truncateCrtRelatedTables(serverUrl);
|
||||
if (error) {
|
||||
return {error: `Resetting CRT on ${serverUrl} failed`};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isUpgrade && meData?.user) {
|
||||
const me = await prepareCommonSystemValues(operator, {currentUserId: meData.user.id});
|
||||
if (me?.length) {
|
||||
await operator.batchRecords(me);
|
||||
}
|
||||
}
|
||||
|
||||
let initialTeamId = currentTeamId;
|
||||
if (!teamData.teams.length) {
|
||||
initialTeamId = '';
|
||||
} else if (!initialTeamId || !teamData.teams.find((t) => t.id === currentTeamId)) {
|
||||
const teamOrderPreference = getPreferenceValue(prefData.preferences || [], Preferences.TEAMS_ORDER, '', '') as string;
|
||||
initialTeamId = selectDefaultTeam(teamData.teams, meData.user.locale, teamOrderPreference, config.ExperimentalPrimaryTeam)?.id || '';
|
||||
}
|
||||
const gqlRoles = [
|
||||
...fetchedData.user?.roles || [],
|
||||
...fetchedData.teamMembers?.map((m) => m.roles).flat() || [],
|
||||
];
|
||||
|
||||
let chData;
|
||||
if (initialTeamId) {
|
||||
const result = await getChannelData(serverUrl, initialTeamId, meData.user.id, false);
|
||||
if ('error' in result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
chData = result.chData;
|
||||
gqlRoles.push(...result.roles);
|
||||
}
|
||||
|
||||
const roles = filterAndTransformRoles(gqlRoles);
|
||||
|
||||
let initialChannelId = currentChannelId;
|
||||
if (initialTeamId !== currentTeamId || !chData?.channels?.find((c) => c.id === currentChannelId)) {
|
||||
initialChannelId = '';
|
||||
if (isTabletDevice && chData?.channels && chData.memberships) {
|
||||
initialChannelId = selectDefaultChannelForTeam(chData.channels, chData.memberships, initialTeamId, roles, meData.user.locale)?.id || '';
|
||||
}
|
||||
}
|
||||
|
||||
let removeTeams: TeamModel[] = [];
|
||||
const removeChannels = await getRemoveChannels(database, chData, initialTeamId, true, syncDatabase);
|
||||
|
||||
if (syncDatabase) {
|
||||
const removeTeamIds = [];
|
||||
|
||||
const removedFromTeam = teamData.memberships?.filter((m) => m.delete_at > 0);
|
||||
if (removedFromTeam?.length) {
|
||||
removeTeamIds.push(...removedFromTeam.map((m) => m.team_id));
|
||||
}
|
||||
|
||||
if (teamData.teams?.length === 0) {
|
||||
// User is no longer a member of any team
|
||||
const myTeams = await queryMyTeams(database).fetch();
|
||||
removeTeamIds.push(...(myTeams?.map((myTeam) => myTeam.id) || []));
|
||||
}
|
||||
|
||||
removeTeams = await teamsToRemove(serverUrl, removeTeamIds);
|
||||
}
|
||||
|
||||
const modelPromises = await prepareModels({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData}, true);
|
||||
modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true}));
|
||||
modelPromises.push(prepareCommonSystemValues(
|
||||
operator,
|
||||
{
|
||||
config,
|
||||
license,
|
||||
currentTeamId: initialTeamId,
|
||||
currentChannelId: initialChannelId,
|
||||
},
|
||||
));
|
||||
|
||||
if (initialTeamId && initialTeamId !== currentTeamId) {
|
||||
const th = addTeamToTeamHistory(operator, initialTeamId, true);
|
||||
modelPromises.push(th);
|
||||
}
|
||||
|
||||
if (initialTeamId !== currentTeamId && initialChannelId) {
|
||||
try {
|
||||
const tch = addChannelToTeamHistory(operator, initialTeamId, initialChannelId, true);
|
||||
modelPromises.push(tch);
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models.flat());
|
||||
}
|
||||
|
||||
const isCRTEnabled = Boolean(prefData.preferences && processIsCRTEnabled(prefData.preferences, config));
|
||||
deferredAppEntryGraphQLActions(serverUrl, 0, meData, teamData, chData, isTabletDevice, initialTeamId, initialChannelId, isCRTEnabled, syncDatabase);
|
||||
|
||||
const timeElapsed = Date.now() - dt;
|
||||
return {time: timeElapsed, hasTeams: Boolean(teamData.teams.length), userId: meData.user.id, error: undefined};
|
||||
};
|
||||
@@ -6,12 +6,13 @@ import {getSessions} from '@actions/remote/session';
|
||||
import {ConfigAndLicenseRequest, fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {prepareCommonSystemValues, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logWarning} from '@utils/log';
|
||||
import {logDebug, logWarning} from '@utils/log';
|
||||
import {scheduleExpiredNotification} from '@utils/notification';
|
||||
|
||||
import {deferredAppEntryActions, entry, EntryResponse} from './common';
|
||||
import {deferredAppEntryActions, entry} from './common';
|
||||
import {graphQLCommon} from './gql_common';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
|
||||
@@ -21,8 +22,13 @@ type AfterLoginArgs = {
|
||||
deviceToken?: string;
|
||||
}
|
||||
|
||||
export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs) {
|
||||
const dt = Date.now();
|
||||
type SpecificAfterLoginArgs = {
|
||||
serverUrl: string;
|
||||
user: UserProfile;
|
||||
clData: ConfigAndLicenseRequest;
|
||||
}
|
||||
|
||||
export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs): Promise<{error?: any; hasTeams?: boolean; time?: number}> {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
@@ -44,18 +50,9 @@ export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs)
|
||||
}
|
||||
|
||||
try {
|
||||
const isTabletDevice = await isTablet();
|
||||
|
||||
// Fetch in parallel server config & license / user preferences / teams / team membership
|
||||
const promises: [Promise<ConfigAndLicenseRequest>, Promise<EntryResponse>] = [
|
||||
fetchConfigAndLicense(serverUrl, true),
|
||||
entry(serverUrl, '', ''),
|
||||
];
|
||||
|
||||
const [clData, entryData] = await Promise.all(promises);
|
||||
|
||||
if ('error' in entryData) {
|
||||
return {error: entryData.error};
|
||||
const clData = await fetchConfigAndLicense(serverUrl, true);
|
||||
if (clData.error) {
|
||||
return {error: clData.error};
|
||||
}
|
||||
|
||||
// schedule local push notification if needed
|
||||
@@ -79,33 +76,51 @@ export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs)
|
||||
}
|
||||
}
|
||||
|
||||
let switchToChannel = false;
|
||||
const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData;
|
||||
if (initialChannelId && isTabletDevice) {
|
||||
switchToChannel = true;
|
||||
switchToChannelById(serverUrl, initialChannelId, initialTeamId);
|
||||
} else {
|
||||
setCurrentTeamAndChannelId(operator, initialTeamId, '');
|
||||
if (clData.config?.FeatureFlagGraphQL === 'true') {
|
||||
const result = await graphQLCommon(serverUrl, false, '', '');
|
||||
if (!result.error) {
|
||||
return result;
|
||||
}
|
||||
logDebug('Error using GraphQL, trying REST', result.error);
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
|
||||
const config = clData.config || {} as ClientConfig;
|
||||
const license = clData.license || {} as ClientLicense;
|
||||
deferredAppEntryActions(serverUrl, 0, user.id, user.locale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchToChannel ? initialChannelId : undefined);
|
||||
|
||||
return {time: Date.now() - dt, hasTeams: Boolean(teamData.teams?.length)};
|
||||
return restLoginEntry({serverUrl, user, clData});
|
||||
} catch (error) {
|
||||
const systemModels = await prepareCommonSystemValues(operator, {
|
||||
config: ({} as ClientConfig),
|
||||
license: ({} as ClientLicense),
|
||||
currentTeamId: '',
|
||||
currentChannelId: '',
|
||||
});
|
||||
if (systemModels) {
|
||||
await operator.batchRecords(systemModels);
|
||||
}
|
||||
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
const restLoginEntry = async ({serverUrl, user, clData}: SpecificAfterLoginArgs) => {
|
||||
const dt = Date.now();
|
||||
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const entryData = await entry(serverUrl, '', '');
|
||||
|
||||
if ('error' in entryData) {
|
||||
return {error: entryData.error};
|
||||
}
|
||||
|
||||
const isTabletDevice = await isTablet();
|
||||
|
||||
const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData;
|
||||
|
||||
let switchToChannel = false;
|
||||
if (initialChannelId && isTabletDevice) {
|
||||
switchToChannel = true;
|
||||
switchToChannelById(serverUrl, initialChannelId, initialTeamId);
|
||||
} else {
|
||||
setCurrentTeamAndChannelId(operator, initialTeamId, '');
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
|
||||
const config = clData.config || {} as ClientConfig;
|
||||
const license = clData.license || {} as ClientLicense;
|
||||
deferredAppEntryActions(serverUrl, 0, user.id, user.locale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchToChannel ? initialChannelId : undefined);
|
||||
|
||||
return {time: Date.now() - dt, hasTeams: Boolean(teamData.teams?.length)};
|
||||
};
|
||||
|
||||
@@ -8,17 +8,19 @@ 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 {getCommonSystemValues, getConfig, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getMyTeamById} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logDebug} from '@utils/log';
|
||||
import {emitNotificationError} from '@utils/notification';
|
||||
import {setThemeDefaults, updateThemeIfNeeded} from '@utils/theme';
|
||||
|
||||
import {deferredAppEntryActions, entry, syncOtherServers} from './common';
|
||||
import {graphQLCommon} from './gql_common';
|
||||
|
||||
export async function pushNotificationEntry(serverUrl: string, notification: NotificationWithData) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
@@ -26,14 +28,11 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const isTabletDevice = await isTablet();
|
||||
|
||||
// We only reach this point if we have a channel Id in the notification payload
|
||||
const channelId = notification.payload!.channel_id!;
|
||||
const rootId = notification.payload!.root_id!;
|
||||
const {database} = operator;
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
|
||||
const currentServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
let isDirectChannel = false;
|
||||
|
||||
@@ -48,13 +47,6 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
|
||||
await DatabaseManager.setActiveServerDatabase(serverUrl);
|
||||
}
|
||||
|
||||
// To make the switch faster we determine if we already have the team & channel
|
||||
const myChannel = await getMyChannel(database, channelId);
|
||||
const myTeam = await getMyTeamById(database, teamId);
|
||||
|
||||
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
const isThreadNotification = isCRTEnabled && Boolean(rootId);
|
||||
|
||||
if (!EphemeralStore.theme) {
|
||||
// When opening the app from a push notification the theme may not be set in the EphemeralStore
|
||||
// causing the goToScreen to use the Appearance theme instead and that causes the screen background color to potentially
|
||||
@@ -69,18 +61,30 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
|
||||
|
||||
await NavigationStore.waitUntilScreenHasLoaded(Screens.HOME);
|
||||
|
||||
let switchedToScreen = false;
|
||||
let switchedToChannel = false;
|
||||
if (myChannel && myTeam) {
|
||||
if (isThreadNotification) {
|
||||
await fetchAndSwitchToThread(serverUrl, rootId, true);
|
||||
} else {
|
||||
switchedToChannel = true;
|
||||
await switchToChannelById(serverUrl, channelId, teamId);
|
||||
const config = await getConfig(database);
|
||||
let result;
|
||||
if (config?.FeatureFlagGraphQL === 'true') {
|
||||
result = await graphQLCommon(serverUrl, true, teamId, channelId);
|
||||
if (result.error) {
|
||||
logDebug('Error using GraphQL, trying REST', result.error);
|
||||
result = restNotificationEntry(serverUrl, teamId, channelId, rootId, isDirectChannel);
|
||||
}
|
||||
switchedToScreen = true;
|
||||
} else {
|
||||
result = restNotificationEntry(serverUrl, teamId, channelId, rootId, isDirectChannel);
|
||||
}
|
||||
|
||||
syncOtherServers(serverUrl);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const restNotificationEntry = async (serverUrl: string, teamId: string, channelId: string, rootId: string, isDirectChannel: boolean) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
const {database} = operator;
|
||||
|
||||
const entryData = await entry(serverUrl, teamId, channelId);
|
||||
if ('error' in entryData) {
|
||||
return {error: entryData.error};
|
||||
@@ -102,7 +106,25 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
|
||||
}
|
||||
}
|
||||
|
||||
const myChannel = await getMyChannel(database, channelId);
|
||||
const myTeam = await getMyTeamById(database, teamId);
|
||||
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
const isThreadNotification = isCRTEnabled && Boolean(rootId);
|
||||
|
||||
let switchedToScreen = false;
|
||||
let switchedToChannel = false;
|
||||
if (myChannel && myTeam) {
|
||||
if (isThreadNotification) {
|
||||
await fetchAndSwitchToThread(serverUrl, rootId, true);
|
||||
} else {
|
||||
switchedToChannel = true;
|
||||
await switchToChannelById(serverUrl, channelId, teamId);
|
||||
}
|
||||
switchedToScreen = true;
|
||||
}
|
||||
|
||||
if (!switchedToScreen) {
|
||||
const isTabletDevice = await isTablet();
|
||||
if (isTabletDevice || (selectedChannelId === channelId)) {
|
||||
// Make switch again to get the missing data and make sure the team is the correct one
|
||||
switchedToScreen = true;
|
||||
@@ -130,8 +152,8 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
|
||||
const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(operator.database))!;
|
||||
const {config, license} = await getCommonSystemValues(operator.database);
|
||||
|
||||
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, switchedToChannel ? selectedChannelId : undefined);
|
||||
syncOtherServers(serverUrl);
|
||||
|
||||
return {userId: currentUserId};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,6 +5,8 @@ import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {deferredAppEntryActions, entry} from '@actions/remote/entry/common';
|
||||
import {graphQLCommon} from '@actions/remote/entry/gql_common';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import {fetchStatusByIds} from '@actions/remote/user';
|
||||
import {loadConfigAndCalls} from '@calls/actions/calls';
|
||||
import {
|
||||
@@ -19,6 +21,7 @@ import {isSupportedServerCalls} from '@calls/utils';
|
||||
import {Events, Screens, WebsocketEvents} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import {getActiveServerUrl, queryActiveServer} from '@queries/app/servers';
|
||||
import {getCurrentChannel} from '@queries/servers/channel';
|
||||
import {
|
||||
@@ -104,21 +107,13 @@ export async function handleClose(serverUrl: string, lastDisconnect: number) {
|
||||
});
|
||||
}
|
||||
|
||||
async function doReconnect(serverUrl: string) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return;
|
||||
}
|
||||
|
||||
async function doReconnectRest(serverUrl: string, operator: ServerDataOperator, currentTeamId: string, currentUserId: string, config: ClientConfig, license: ClientLicense, lastDisconnectedAt: number) {
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (!appDatabase) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
const tabletDevice = await isTablet();
|
||||
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
|
||||
resetWebSocketLastDisconnected(operator);
|
||||
const currentTeam = await getCurrentTeam(database);
|
||||
const currentChannel = await getCurrentChannel(database);
|
||||
const currentActiveServerUrl = await getActiveServerUrl(DatabaseManager.appDatabase!.database);
|
||||
@@ -152,6 +147,8 @@ async function doReconnect(serverUrl: string) {
|
||||
await popToRoot();
|
||||
}
|
||||
|
||||
const tabletDevice = await isTablet();
|
||||
|
||||
if (tabletDevice && initialChannelId) {
|
||||
switchedToChannel = true;
|
||||
switchToChannelById(serverUrl, initialChannelId, initialTeamId);
|
||||
@@ -167,8 +164,7 @@ async function doReconnect(serverUrl: string) {
|
||||
await operator.batchRecords(models);
|
||||
logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(database))!;
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
const {locale: currentUserLocale} = (await getCurrentUser(database))!;
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchedToChannel ? initialChannelId : undefined);
|
||||
|
||||
if (isSupportedServerCalls(config?.Version)) {
|
||||
@@ -178,6 +174,33 @@ async function doReconnect(serverUrl: string) {
|
||||
// https://mattermost.atlassian.net/browse/MM-41520
|
||||
}
|
||||
|
||||
async function doReconnect(serverUrl: string) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
const system = await getCommonSystemValues(database);
|
||||
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
|
||||
|
||||
resetWebSocketLastDisconnected(operator);
|
||||
let {config, license} = await fetchConfigAndLicense(serverUrl);
|
||||
if (!config) {
|
||||
config = system.config;
|
||||
}
|
||||
|
||||
if (!license) {
|
||||
license = system.license;
|
||||
}
|
||||
|
||||
if (config.FeatureFlagGraphQL === 'true') {
|
||||
await graphQLCommon(serverUrl, true, system.currentTeamId, system.currentChannelId);
|
||||
} else {
|
||||
await doReconnectRest(serverUrl, operator, system.currentTeamId, system.currentUserId, config, license, lastDisconnectedAt);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
switch (msg.event) {
|
||||
case WebsocketEvents.POSTED:
|
||||
|
||||
16
app/client/graphQL/constants.ts
Normal file
16
app/client/graphQL/constants.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const QUERY_ENTRY = 'gql_m_entry';
|
||||
export const QUERY_CHANNELS = 'gql_m_channels';
|
||||
export const QUERY_CHANNELS_NEXT = 'gql_m_channels_next';
|
||||
export const QUERY_ALL_CHANNELS = 'gql_m_all_channels';
|
||||
export const QUERY_ALL_CHANNELS_NEXT = 'gql_m_all_channels_next';
|
||||
|
||||
export default {
|
||||
QUERY_ENTRY,
|
||||
QUERY_CHANNELS,
|
||||
QUERY_CHANNELS_NEXT,
|
||||
QUERY_ALL_CHANNELS,
|
||||
QUERY_ALL_CHANNELS_NEXT,
|
||||
};
|
||||
371
app/client/graphQL/entry.ts
Normal file
371
app/client/graphQL/entry.ts
Normal file
@@ -0,0 +1,371 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {MEMBERS_PER_PAGE} from '@constants/graphql';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
|
||||
import {Client} from '../rest';
|
||||
|
||||
import QueryNames from './constants';
|
||||
|
||||
const doGQLQuery = async (serverUrl: string, query: string, variables: {[name: string]: any}, operationName: string) => {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await client.doFetch('/api/v5/graphql', {method: 'post', body: JSON.stringify({query, variables, operationName})}) as GQLResponse;
|
||||
return response;
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const gqlEntry = async (serverUrl: string) => {
|
||||
return doGQLQuery(serverUrl, entryQuery, {}, QueryNames.QUERY_ENTRY);
|
||||
};
|
||||
|
||||
export const gqlEntryChannels = async (serverUrl: string, teamId: string) => {
|
||||
const variables = {
|
||||
teamId,
|
||||
exclude: false,
|
||||
perPage: MEMBERS_PER_PAGE,
|
||||
};
|
||||
const response = await doGQLQuery(serverUrl, channelsQuery, variables, QueryNames.QUERY_CHANNELS);
|
||||
if ('error' in response || response.errors) {
|
||||
return response;
|
||||
}
|
||||
|
||||
let members = response.data.channelMembers;
|
||||
|
||||
while (members?.length === MEMBERS_PER_PAGE) {
|
||||
let pageResponse;
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
pageResponse = await gqlEntryChannelsNextPage(serverUrl, teamId, members[members.length - 1].cursor!, false);
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
|
||||
if ('error' in pageResponse) {
|
||||
break;
|
||||
}
|
||||
|
||||
members = pageResponse.data.channelMembers!;
|
||||
response.data.channelMembers?.push(...members);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
const gqlEntryChannelsNextPage = async (serverUrl: string, teamId: string, cursor: string, exclude: boolean) => {
|
||||
const variables = {
|
||||
teamId,
|
||||
exclude,
|
||||
perPage: MEMBERS_PER_PAGE,
|
||||
cursor,
|
||||
};
|
||||
return doGQLQuery(serverUrl, nextPageChannelsQuery, variables, QueryNames.QUERY_CHANNELS_NEXT);
|
||||
};
|
||||
|
||||
export const gqlOtherChannels = async (serverUrl: string, teamId: string) => {
|
||||
const variables = {
|
||||
teamId,
|
||||
exclude: true,
|
||||
perPage: MEMBERS_PER_PAGE,
|
||||
};
|
||||
const response = await doGQLQuery(serverUrl, channelsQuery, variables, QueryNames.QUERY_CHANNELS);
|
||||
if ('error' in response || response.errors) {
|
||||
return response;
|
||||
}
|
||||
|
||||
let members = response.data.channelMembers;
|
||||
|
||||
while (members?.length === MEMBERS_PER_PAGE) {
|
||||
let pageResponse;
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
pageResponse = await gqlEntryChannelsNextPage(serverUrl, teamId, members[members.length - 1].cursor!, true);
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
|
||||
if ('error' in pageResponse || 'errors' in pageResponse) {
|
||||
break;
|
||||
}
|
||||
|
||||
members = pageResponse.data.channelMembers!;
|
||||
response.data.channelMembers?.push(...members);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
export const gqlAllChannels = async (serverUrl: string) => {
|
||||
const variables = {
|
||||
perPage: MEMBERS_PER_PAGE,
|
||||
};
|
||||
const response = await doGQLQuery(serverUrl, allChannelsQuery, variables, QueryNames.QUERY_ALL_CHANNELS);
|
||||
if ('error' in response || response.errors) {
|
||||
return response;
|
||||
}
|
||||
|
||||
let members = response.data.channelMembers;
|
||||
|
||||
while (members?.length === MEMBERS_PER_PAGE) {
|
||||
let pageResponse;
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
pageResponse = await gqlAllChannelsNextPage(serverUrl, members[members.length - 1].cursor!);
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
|
||||
if ('error' in pageResponse || 'errors' in pageResponse) {
|
||||
break;
|
||||
}
|
||||
|
||||
members = pageResponse.data.channelMembers!;
|
||||
response.data.channelMembers?.push(...members);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
const gqlAllChannelsNextPage = async (serverUrl: string, cursor: string) => {
|
||||
const variables = {
|
||||
perPage: MEMBERS_PER_PAGE,
|
||||
cursor,
|
||||
};
|
||||
return doGQLQuery(serverUrl, nextPageAllChannelsQuery, variables, QueryNames.QUERY_ALL_CHANNELS_NEXT);
|
||||
};
|
||||
|
||||
const entryQuery = `
|
||||
query ${QueryNames.QUERY_ENTRY} {
|
||||
config
|
||||
license
|
||||
user(id:"me") {
|
||||
id
|
||||
createAt
|
||||
updateAt
|
||||
deleteAt
|
||||
username
|
||||
authService
|
||||
email
|
||||
emailVerified
|
||||
nickname
|
||||
firstName
|
||||
lastName
|
||||
position
|
||||
roles {
|
||||
id
|
||||
name
|
||||
permissions
|
||||
}
|
||||
locale
|
||||
notifyProps
|
||||
props
|
||||
timezone
|
||||
isBot
|
||||
lastPictureUpdate
|
||||
remoteId
|
||||
status {
|
||||
status
|
||||
}
|
||||
botDescription
|
||||
botLastIconUpdate
|
||||
preferences{
|
||||
category
|
||||
name
|
||||
value
|
||||
userId
|
||||
}
|
||||
sessions {
|
||||
createAt
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
teamMembers(userId:"me") {
|
||||
deleteAt
|
||||
roles {
|
||||
id
|
||||
name
|
||||
permissions
|
||||
}
|
||||
team {
|
||||
id
|
||||
description
|
||||
displayName
|
||||
name
|
||||
type
|
||||
allowedDomains
|
||||
lastTeamIconUpdate
|
||||
groupConstrained
|
||||
allowOpenInvite
|
||||
createAt
|
||||
updateAt
|
||||
deleteAt
|
||||
schemeId
|
||||
policyId
|
||||
cloudLimitsArchived
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const channelsQuery = `
|
||||
query ${QueryNames.QUERY_CHANNELS}($teamId: String!, $perPage: Int!, $exclude: Boolean!) {
|
||||
channelMembers(userId:"me", first:$perPage, teamId:$teamId, excludeTeam:$exclude) {
|
||||
cursor
|
||||
msgCount
|
||||
msgCountRoot
|
||||
mentionCount
|
||||
lastViewedAt
|
||||
notifyProps
|
||||
roles {
|
||||
id
|
||||
name
|
||||
permissions
|
||||
}
|
||||
channel {
|
||||
id
|
||||
header
|
||||
purpose
|
||||
type
|
||||
createAt
|
||||
creatorId
|
||||
deleteAt
|
||||
displayName
|
||||
prettyDisplayName
|
||||
groupConstrained
|
||||
name
|
||||
shared
|
||||
lastPostAt
|
||||
totalMsgCount
|
||||
totalMsgCountRoot
|
||||
lastRootPostAt
|
||||
team {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
sidebarCategories(userId:"me", teamId:$teamId, excludeTeam:$exclude) {
|
||||
displayName
|
||||
id
|
||||
sorting
|
||||
type
|
||||
muted
|
||||
collapsed
|
||||
channelIds
|
||||
teamId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const nextPageChannelsQuery = `
|
||||
query ${QueryNames.QUERY_CHANNELS_NEXT}($teamId: String!, $perPage: Int!, $exclude: Boolean!, $cursor: String!) {
|
||||
channelMembers(userId:"me", first:$perPage, after:$cursor, teamId:$teamId, excludeTeam:$exclude) {
|
||||
cursor
|
||||
msgCount
|
||||
msgCountRoot
|
||||
mentionCount
|
||||
lastViewedAt
|
||||
notifyProps
|
||||
roles {
|
||||
id
|
||||
name
|
||||
permissions
|
||||
}
|
||||
channel {
|
||||
id
|
||||
header
|
||||
purpose
|
||||
type
|
||||
createAt
|
||||
creatorId
|
||||
deleteAt
|
||||
displayName
|
||||
prettyDisplayName
|
||||
groupConstrained
|
||||
name
|
||||
shared
|
||||
lastPostAt
|
||||
totalMsgCount
|
||||
totalMsgCountRoot
|
||||
lastRootPostAt
|
||||
team {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const allChannelsQuery = `
|
||||
query ${QueryNames.QUERY_ALL_CHANNELS}($perPage: Int!){
|
||||
channelMembers(userId:"me", first:$perPage) {
|
||||
cursor
|
||||
msgCount
|
||||
msgCountRoot
|
||||
mentionCount
|
||||
lastViewedAt
|
||||
notifyProps
|
||||
channel {
|
||||
id
|
||||
header
|
||||
purpose
|
||||
type
|
||||
createAt
|
||||
creatorId
|
||||
deleteAt
|
||||
displayName
|
||||
prettyDisplayName
|
||||
groupConstrained
|
||||
name
|
||||
shared
|
||||
lastPostAt
|
||||
totalMsgCount
|
||||
totalMsgCountRoot
|
||||
lastRootPostAt
|
||||
team {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const nextPageAllChannelsQuery = `
|
||||
query ${QueryNames.QUERY_ALL_CHANNELS_NEXT}($perPage: Int!, $cursor: String!) {
|
||||
channelMembers(userId:"me", first:$perPage, after:$cursor) {
|
||||
cursor
|
||||
msgCount
|
||||
msgCountRoot
|
||||
mentionCount
|
||||
lastViewedAt
|
||||
notifyProps
|
||||
channel {
|
||||
id
|
||||
header
|
||||
purpose
|
||||
type
|
||||
createAt
|
||||
creatorId
|
||||
deleteAt
|
||||
displayName
|
||||
prettyDisplayName
|
||||
groupConstrained
|
||||
name
|
||||
shared
|
||||
lastPostAt
|
||||
totalMsgCount
|
||||
totalMsgCountRoot
|
||||
lastRootPostAt
|
||||
team {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -5,7 +5,7 @@ import Database from '@nozbe/watermelondb/Database';
|
||||
import React from 'react';
|
||||
|
||||
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
|
||||
import {CustomStatusDuration} from '@constants';
|
||||
import {CustomStatusDurationEnum} from '@constants/custom_status';
|
||||
import {renderWithEverything} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('components/custom_status/custom_status_emoji', () => {
|
||||
const customStatus: UserCustomStatus = {
|
||||
emoji: 'calendar',
|
||||
text: 'In a meeting',
|
||||
duration: CustomStatusDuration.DONT_CLEAR,
|
||||
duration: CustomStatusDurationEnum.DONT_CLEAR,
|
||||
};
|
||||
it('should match snapshot', () => {
|
||||
const wrapper = renderWithEverything(
|
||||
|
||||
@@ -71,7 +71,7 @@ const Image = ({author, forwardRef, iconSize, size, source, url}: Props) => {
|
||||
if (isBot) {
|
||||
lastPictureUpdate = ('isBot' in author) ? author.props?.bot_last_icon_update : author.bot_last_icon_update || 0;
|
||||
} else {
|
||||
lastPictureUpdate = ('lastPictureUpdate' in author) ? author.lastPictureUpdate : author.last_picture_update;
|
||||
lastPictureUpdate = ('lastPictureUpdate' in author) ? author.lastPictureUpdate : author.last_picture_update || 0;
|
||||
}
|
||||
|
||||
const pictureUrl = client.getProfilePictureUrl(author.id, lastPictureUpdate);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {t} from '@i18n';
|
||||
|
||||
export enum CustomStatusDuration {
|
||||
export enum CustomStatusDurationEnum {
|
||||
DONT_CLEAR = '',
|
||||
THIRTY_MINUTES = 'thirty_minutes',
|
||||
ONE_HOUR = 'one_hour',
|
||||
@@ -21,9 +21,9 @@ const {
|
||||
TODAY,
|
||||
THIS_WEEK,
|
||||
DATE_AND_TIME,
|
||||
} = CustomStatusDuration;
|
||||
} = CustomStatusDurationEnum;
|
||||
|
||||
export const CST = {
|
||||
export const CST: {[key in CustomStatusDuration]: {id: string; defaultMessage: string}} = {
|
||||
[DONT_CLEAR]: {
|
||||
id: t('custom_status.expiry_dropdown.dont_clear'),
|
||||
defaultMessage: "Don't clear",
|
||||
|
||||
4
app/constants/graphql.ts
Normal file
4
app/constants/graphql.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const MEMBERS_PER_PAGE = 200;
|
||||
@@ -6,7 +6,6 @@ import Apps from './apps';
|
||||
import Categories from './categories';
|
||||
import Channel from './channel';
|
||||
import Config from './config';
|
||||
import {CustomStatusDuration} from './custom_status';
|
||||
import Database from './database';
|
||||
import DateTime from './datetime';
|
||||
import DeepLink from './deep_linking';
|
||||
@@ -40,7 +39,6 @@ export {
|
||||
Categories,
|
||||
Channel,
|
||||
Config,
|
||||
CustomStatusDuration,
|
||||
Database,
|
||||
DateTime,
|
||||
DeepLink,
|
||||
|
||||
@@ -33,7 +33,7 @@ export const transformUserRecord = ({action, database, value}: TransformerArgs):
|
||||
user.firstName = raw.first_name;
|
||||
user.isGuest = raw.roles.includes('system_guest');
|
||||
user.lastName = raw.last_name;
|
||||
user.lastPictureUpdate = raw.last_picture_update;
|
||||
user.lastPictureUpdate = raw.last_picture_update || 0;
|
||||
user.locale = raw.locale;
|
||||
user.nickname = raw.nickname;
|
||||
user.position = raw?.position ?? '';
|
||||
@@ -41,7 +41,7 @@ export const transformUserRecord = ({action, database, value}: TransformerArgs):
|
||||
user.username = raw.username;
|
||||
user.notifyProps = raw.notify_props;
|
||||
user.timezone = raw.timezone || null;
|
||||
user.isBot = raw.is_bot;
|
||||
user.isBot = raw.is_bot ?? false;
|
||||
user.remoteId = raw?.remote_id ?? null;
|
||||
if (raw.status) {
|
||||
user.status = raw.status;
|
||||
|
||||
@@ -62,15 +62,18 @@ export function prepareMissingChannelsForAllTeams(operator: ServerDataOperator,
|
||||
}
|
||||
}
|
||||
|
||||
export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, teamId: string, channels: Channel[], channelMembers: ChannelMembership[], isCRTEnabled?: boolean) => {
|
||||
export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, teamId: string, channels: Channel[], channelMembers: ChannelMembership[], isCRTEnabled?: boolean, isGraphQL = false) => {
|
||||
const {database} = operator;
|
||||
const allChannelsForTeam = (await queryAllChannelsForTeam(database, teamId).fetch()).
|
||||
|
||||
const channelsQuery = isGraphQL ? queryAllChannels(database) : queryAllChannelsForTeam(database, teamId);
|
||||
const allChannelsForTeam = (await channelsQuery.fetch()).
|
||||
reduce((map: Record<string, ChannelModel>, channel) => {
|
||||
map[channel.id] = channel;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
const allChannelsInfoForTeam = (await queryAllChannelsInfoForTeam(database, teamId).fetch()).
|
||||
const channelInfosQuery = isGraphQL ? queryAllChannelsInfo(database) : queryAllChannelsInfoForTeam(database, teamId);
|
||||
const allChannelsInfoForTeam = (await channelInfosQuery.fetch()).
|
||||
reduce((map: Record<string, ChannelInfoModel>, info) => {
|
||||
map[info.id] = info;
|
||||
return map;
|
||||
@@ -161,10 +164,18 @@ export const prepareDeleteChannel = async (channel: ChannelModel): Promise<Model
|
||||
return preparedModels;
|
||||
};
|
||||
|
||||
export const queryAllChannels = (database: Database) => {
|
||||
return database.get<ChannelModel>(CHANNEL).query();
|
||||
};
|
||||
|
||||
export const queryAllChannelsForTeam = (database: Database, teamId: string) => {
|
||||
return database.get<ChannelModel>(CHANNEL).query(Q.where('team_id', teamId));
|
||||
};
|
||||
|
||||
export const queryAllChannelsInfo = (database: Database) => {
|
||||
return database.get<ChannelInfoModel>(CHANNEL_INFO).query();
|
||||
};
|
||||
|
||||
export const queryAllChannelsInfoForTeam = (database: Database, teamId: string) => {
|
||||
return database.get<ChannelInfoModel>(CHANNEL_INFO).query(
|
||||
Q.on(CHANNEL, Q.where('team_id', teamId)),
|
||||
|
||||
@@ -42,7 +42,7 @@ const {
|
||||
MY_CHANNEL,
|
||||
} = MM_TABLES.SERVER;
|
||||
|
||||
export async function prepareModels({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData, isCRTEnabled}: PrepareModelsArgs): Promise<Array<Promise<Model[]>>> {
|
||||
export async function prepareModels({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData, isCRTEnabled}: PrepareModelsArgs, isGraphQL = false): Promise<Array<Promise<Model[]>>> {
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
|
||||
if (removeTeams?.length) {
|
||||
@@ -66,8 +66,12 @@ export async function prepareModels({operator, initialTeamId, removeTeams, remov
|
||||
modelPromises.push(prepareCategoryChannels(operator, chData.categories));
|
||||
}
|
||||
|
||||
if (initialTeamId && chData?.channels?.length && chData.memberships?.length) {
|
||||
modelPromises.push(...await prepareMyChannelsForTeam(operator, initialTeamId, chData.channels, chData.memberships, isCRTEnabled));
|
||||
if (chData?.channels?.length && chData.memberships?.length) {
|
||||
if (isGraphQL) {
|
||||
modelPromises.push(...await prepareMyChannelsForTeam(operator, '', chData.channels, chData.memberships, isCRTEnabled, true));
|
||||
} else if (initialTeamId) {
|
||||
modelPromises.push(...await prepareMyChannelsForTeam(operator, initialTeamId, chData.channels, chData.memberships, isCRTEnabled, false));
|
||||
}
|
||||
}
|
||||
|
||||
if (prefData?.preferences?.length) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {Text, TouchableOpacity, View} from 'react-native';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import CustomStatusExpiry from '@components/custom_status/custom_status_expiry';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {CustomStatusDuration, CST} from '@constants/custom_status';
|
||||
import {CST} from '@constants/custom_status';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type {Moment} from 'moment-timezone';
|
||||
@@ -54,7 +54,7 @@ const ClearAfter = ({duration, expiresAt, onOpenClearAfterModal, theme}: Props)
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const renderClearAfterTime = () => {
|
||||
if (duration && duration === CustomStatusDuration.DATE_AND_TIME) {
|
||||
if (duration && duration === 'date_and_time') {
|
||||
return (
|
||||
<View style={style.expiryTime}>
|
||||
<CustomStatusExpiry
|
||||
|
||||
@@ -8,12 +8,12 @@ import {TouchableOpacity, View} from 'react-native';
|
||||
import ClearButton from '@components/custom_status/clear_button';
|
||||
import CustomStatusText from '@components/custom_status/custom_status_text';
|
||||
import Emoji from '@components/emoji';
|
||||
import {CST, CustomStatusDuration} from '@constants/custom_status';
|
||||
import {CST} from '@constants/custom_status';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
duration: CustomStatusDuration;
|
||||
duration?: CustomStatusDuration;
|
||||
emoji?: string;
|
||||
expires_at?: string;
|
||||
handleClear?: (status: UserCustomStatus) => void;
|
||||
@@ -75,7 +75,7 @@ const CustomStatusSuggestion = ({duration, emoji, expires_at, handleClear, handl
|
||||
}
|
||||
}, []);
|
||||
|
||||
const showCustomStatus = Boolean(duration && duration !== CustomStatusDuration.DATE_AND_TIME && isExpirySupported);
|
||||
const showCustomStatus = Boolean(duration && duration !== 'date_and_time' && isExpirySupported);
|
||||
|
||||
const clearButton =
|
||||
handleClear && expires_at ? (
|
||||
@@ -118,7 +118,7 @@ const CustomStatusSuggestion = ({duration, emoji, expires_at, handleClear, handl
|
||||
{showCustomStatus && (
|
||||
<View style={{paddingTop: 5}}>
|
||||
<CustomStatusText
|
||||
text={intl.formatMessage(CST[duration])}
|
||||
text={intl.formatMessage(CST[duration!])}
|
||||
theme={theme}
|
||||
textStyle={style.customStatusDuration}
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,6 @@ import {IntlShape} from 'react-intl';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {CustomStatusDuration} from '@constants/custom_status';
|
||||
import {t} from '@i18n';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
@@ -24,7 +23,7 @@ type DefaultUserCustomStatus = {
|
||||
emoji: string;
|
||||
message: string;
|
||||
messageDefault: string;
|
||||
durationDefault: string;
|
||||
durationDefault: CustomStatusDuration;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
@@ -49,11 +48,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
});
|
||||
|
||||
const defaultCustomStatusSuggestions: DefaultUserCustomStatus[] = [
|
||||
{emoji: 'calendar', message: t('custom_status.suggestions.in_a_meeting'), messageDefault: 'In a meeting', durationDefault: CustomStatusDuration.ONE_HOUR},
|
||||
{emoji: 'hamburger', message: t('custom_status.suggestions.out_for_lunch'), messageDefault: 'Out for lunch', durationDefault: CustomStatusDuration.THIRTY_MINUTES},
|
||||
{emoji: 'sneezing_face', message: t('custom_status.suggestions.out_sick'), messageDefault: 'Out sick', durationDefault: CustomStatusDuration.TODAY},
|
||||
{emoji: 'house', message: t('custom_status.suggestions.working_from_home'), messageDefault: 'Working from home', durationDefault: CustomStatusDuration.TODAY},
|
||||
{emoji: 'palm_tree', message: t('custom_status.suggestions.on_a_vacation'), messageDefault: 'On a vacation', durationDefault: CustomStatusDuration.THIS_WEEK},
|
||||
{emoji: 'calendar', message: t('custom_status.suggestions.in_a_meeting'), messageDefault: 'In a meeting', durationDefault: 'one_hour'},
|
||||
{emoji: 'hamburger', message: t('custom_status.suggestions.out_for_lunch'), messageDefault: 'Out for lunch', durationDefault: 'thirty_minutes'},
|
||||
{emoji: 'sneezing_face', message: t('custom_status.suggestions.out_sick'), messageDefault: 'Out sick', durationDefault: 'today'},
|
||||
{emoji: 'house', message: t('custom_status.suggestions.working_from_home'), messageDefault: 'Working from home', durationDefault: 'today'},
|
||||
{emoji: 'palm_tree', message: t('custom_status.suggestions.on_a_vacation'), messageDefault: 'On a vacation', durationDefault: 'this_week'},
|
||||
];
|
||||
|
||||
const CustomStatusSuggestions = ({
|
||||
@@ -72,11 +71,11 @@ const CustomStatusSuggestions = ({
|
||||
text: intl.formatMessage({id: status.message, defaultMessage: status.messageDefault}),
|
||||
duration: status.durationDefault,
|
||||
})).
|
||||
filter((status: UserCustomStatus) => !recentCustomStatusTexts.has(status.text)).
|
||||
map((status: UserCustomStatus, index: number, arr: UserCustomStatus[]) => (
|
||||
filter((status) => !recentCustomStatusTexts.has(status.text)).
|
||||
map((status, index, arr) => (
|
||||
<CustomStatusSuggestion
|
||||
key={status.text}
|
||||
handleSuggestionClick={onHandleCustomStatusSuggestionClick} // this.handleCustomStatusSuggestionClick
|
||||
handleSuggestionClick={onHandleCustomStatusSuggestionClick}
|
||||
emoji={status.emoji}
|
||||
text={status.text}
|
||||
theme={theme}
|
||||
|
||||
@@ -16,8 +16,8 @@ import {updateLocalCustomStatus} from '@actions/local/user';
|
||||
import {removeRecentCustomStatus, updateCustomStatus, unsetCustomStatus} from '@actions/remote/user';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TabletTitle from '@components/tablet_title';
|
||||
import {CustomStatusDuration, Events, Screens} from '@constants';
|
||||
import {SET_CUSTOM_STATUS_FAILURE} from '@constants/custom_status';
|
||||
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';
|
||||
@@ -25,6 +25,7 @@ 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 {logDebug} from '@utils/log';
|
||||
import {mergeNavigationOptions} from '@utils/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
@@ -60,8 +61,7 @@ type State = {
|
||||
expires_at: Moment;
|
||||
};
|
||||
|
||||
const {DONT_CLEAR, THIRTY_MINUTES, ONE_HOUR, FOUR_HOURS, TODAY, THIS_WEEK, DATE_AND_TIME} = CustomStatusDuration;
|
||||
const DEFAULT_DURATION: CustomStatusDuration = TODAY;
|
||||
const DEFAULT_DURATION: CustomStatusDuration = 'today';
|
||||
const BTN_UPDATE_STATUS = 'update-custom-status';
|
||||
const edges: Edge[] = ['bottom', 'left', 'right'];
|
||||
|
||||
@@ -129,12 +129,12 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
||||
|
||||
let initialCustomExpiryTime: Moment = getRoundedTime(currentTime);
|
||||
const isCurrentCustomStatusSet = !this.isCustomStatusExpired && (customStatus?.text || customStatus?.emoji);
|
||||
if (isCurrentCustomStatusSet && customStatus?.duration === DATE_AND_TIME && customStatus?.expires_at) {
|
||||
if (isCurrentCustomStatusSet && customStatus?.duration === 'date_and_time' && customStatus?.expires_at) {
|
||||
initialCustomExpiryTime = moment(customStatus?.expires_at);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
duration: isCurrentCustomStatusSet ? customStatus?.duration ?? DONT_CLEAR : DEFAULT_DURATION,
|
||||
duration: isCurrentCustomStatusSet ? customStatus?.duration ?? CustomStatusDurationEnum.DONT_CLEAR : DEFAULT_DURATION,
|
||||
emoji: isCurrentCustomStatusSet ? customStatus?.emoji : '',
|
||||
expires_at: initialCustomExpiryTime,
|
||||
text: isCurrentCustomStatusSet ? customStatus?.text : '',
|
||||
@@ -186,7 +186,7 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
||||
if (isStatusSet) {
|
||||
let isStatusSame = customStatus?.emoji === emoji && customStatus?.text === text && customStatus?.duration === duration;
|
||||
const expiresAt = this.calculateExpiryTime(duration);
|
||||
if (isStatusSame && duration === DATE_AND_TIME) {
|
||||
if (isStatusSame && duration === 'date_and_time') {
|
||||
isStatusSame = customStatus?.expires_at === expiresAt;
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
||||
const status: UserCustomStatus = {
|
||||
emoji: emoji || 'speech_balloon',
|
||||
text: text?.trim(),
|
||||
duration: DONT_CLEAR,
|
||||
duration: CustomStatusDurationEnum.DONT_CLEAR,
|
||||
};
|
||||
|
||||
if (customStatusExpirySupported) {
|
||||
@@ -210,7 +210,7 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
||||
updateLocalCustomStatus(serverUrl, currentUser, status);
|
||||
|
||||
this.setState({
|
||||
duration: status.duration,
|
||||
duration: status.duration!,
|
||||
emoji: status.emoji,
|
||||
expires_at: moment(status.expires_at),
|
||||
text: status.text,
|
||||
@@ -238,19 +238,19 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
||||
|
||||
const {expires_at} = this.state;
|
||||
switch (duration) {
|
||||
case THIRTY_MINUTES:
|
||||
case 'thirty_minutes':
|
||||
return currentTime.add(30, 'minutes').seconds(0).milliseconds(0).toISOString();
|
||||
case ONE_HOUR:
|
||||
case 'one_hour':
|
||||
return currentTime.add(1, 'hour').seconds(0).milliseconds(0).toISOString();
|
||||
case FOUR_HOURS:
|
||||
case 'four_hours':
|
||||
return currentTime.add(4, 'hours').seconds(0).milliseconds(0).toISOString();
|
||||
case TODAY:
|
||||
case 'today':
|
||||
return currentTime.endOf('day').toISOString();
|
||||
case THIS_WEEK:
|
||||
case 'this_week':
|
||||
return currentTime.endOf('week').toISOString();
|
||||
case DATE_AND_TIME:
|
||||
case 'date_and_time':
|
||||
return expires_at.toISOString();
|
||||
case DONT_CLEAR:
|
||||
case CustomStatusDurationEnum.DONT_CLEAR:
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -266,13 +266,18 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
||||
|
||||
handleCustomStatusSuggestionClick = (status: UserCustomStatus) => {
|
||||
const {emoji, text, duration} = status;
|
||||
if (!duration) {
|
||||
// This should never happen, but we add a safeguard here
|
||||
logDebug('clicked on a custom status with no duration');
|
||||
return;
|
||||
}
|
||||
this.setState({emoji, text, duration});
|
||||
};
|
||||
|
||||
handleRecentCustomStatusSuggestionClick = (status: UserCustomStatus) => {
|
||||
const {emoji, text, duration} = status;
|
||||
this.setState({emoji, text, duration: duration || DONT_CLEAR});
|
||||
if (duration === DATE_AND_TIME) {
|
||||
this.setState({emoji, text, duration: duration || CustomStatusDurationEnum.DONT_CLEAR});
|
||||
if (duration === 'date_and_time') {
|
||||
this.openClearAfterModal();
|
||||
}
|
||||
};
|
||||
@@ -295,7 +300,7 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
||||
handleClearAfterClick = (duration: CustomStatusDuration, expires_at: string) =>
|
||||
this.setState({
|
||||
duration,
|
||||
expires_at: duration === DATE_AND_TIME && expires_at ? moment(expires_at) : this.state.expires_at,
|
||||
expires_at: duration === 'date_and_time' && expires_at ? moment(expires_at) : this.state.expires_at,
|
||||
});
|
||||
|
||||
openClearAfterModal = async () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {View, TouchableOpacity} from 'react-native';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import CustomStatusExpiry from '@components/custom_status/custom_status_expiry';
|
||||
import CustomStatusText from '@components/custom_status/custom_status_text';
|
||||
import {CustomStatusDuration, CST} from '@constants/custom_status';
|
||||
import {CST, CustomStatusDurationEnum} from '@constants/custom_status';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
@@ -74,13 +74,13 @@ const ClearAfterMenuItem = ({currentUser, duration, expiryTime = '', handleItemC
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const expiryMenuItems: { [key in CustomStatusDuration]: string } = {
|
||||
[CustomStatusDuration.DONT_CLEAR]: intl.formatMessage(CST[CustomStatusDuration.DONT_CLEAR]),
|
||||
[CustomStatusDuration.THIRTY_MINUTES]: intl.formatMessage(CST[CustomStatusDuration.THIRTY_MINUTES]),
|
||||
[CustomStatusDuration.ONE_HOUR]: intl.formatMessage(CST[CustomStatusDuration.ONE_HOUR]),
|
||||
[CustomStatusDuration.FOUR_HOURS]: intl.formatMessage(CST[CustomStatusDuration.FOUR_HOURS]),
|
||||
[CustomStatusDuration.TODAY]: intl.formatMessage(CST[CustomStatusDuration.TODAY]),
|
||||
[CustomStatusDuration.THIS_WEEK]: intl.formatMessage(CST[CustomStatusDuration.THIS_WEEK]),
|
||||
[CustomStatusDuration.DATE_AND_TIME]: intl.formatMessage({id: 'custom_status.expiry_dropdown.custom', defaultMessage: 'Custom'}),
|
||||
[CustomStatusDurationEnum.DONT_CLEAR]: intl.formatMessage(CST[CustomStatusDurationEnum.DONT_CLEAR]),
|
||||
[CustomStatusDurationEnum.THIRTY_MINUTES]: intl.formatMessage(CST[CustomStatusDurationEnum.THIRTY_MINUTES]),
|
||||
[CustomStatusDurationEnum.ONE_HOUR]: intl.formatMessage(CST[CustomStatusDurationEnum.ONE_HOUR]),
|
||||
[CustomStatusDurationEnum.FOUR_HOURS]: intl.formatMessage(CST[CustomStatusDurationEnum.FOUR_HOURS]),
|
||||
[CustomStatusDurationEnum.TODAY]: intl.formatMessage(CST[CustomStatusDurationEnum.TODAY]),
|
||||
[CustomStatusDurationEnum.THIS_WEEK]: intl.formatMessage(CST[CustomStatusDurationEnum.THIS_WEEK]),
|
||||
[CustomStatusDurationEnum.DATE_AND_TIME]: intl.formatMessage({id: 'custom_status.expiry_dropdown.custom', defaultMessage: 'Custom'}),
|
||||
};
|
||||
|
||||
const handleClick = preventDoubleTap(() => {
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
Options,
|
||||
} from 'react-native-navigation';
|
||||
|
||||
import {CustomStatusDuration} from '@constants/custom_status';
|
||||
import {CustomStatusDurationEnum} from '@constants/custom_status';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
import {dismissModal, popTopScreen} from '@screens/navigation';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
@@ -140,7 +140,7 @@ class ClearAfterModal extends NavigationComponent<Props, State> {
|
||||
this.setState({
|
||||
duration,
|
||||
expiresAt,
|
||||
showExpiryTime: duration === CustomStatusDuration.DATE_AND_TIME && expiresAt !== '',
|
||||
showExpiryTime: duration === 'date_and_time' && expiresAt !== '',
|
||||
});
|
||||
|
||||
renderClearAfterMenu = () => {
|
||||
@@ -149,7 +149,7 @@ class ClearAfterModal extends NavigationComponent<Props, State> {
|
||||
|
||||
const {duration} = this.state;
|
||||
|
||||
const clearAfterMenu = Object.values(CustomStatusDuration).map(
|
||||
const clearAfterMenu = Object.values(CustomStatusDurationEnum).map(
|
||||
(item, index, arr) => {
|
||||
if (index === arr.length - 1) {
|
||||
return null;
|
||||
@@ -195,12 +195,12 @@ class ClearAfterModal extends NavigationComponent<Props, State> {
|
||||
<View style={style.block}>
|
||||
<ClearAfterMenuItem
|
||||
currentUser={currentUser}
|
||||
duration={CustomStatusDuration.DATE_AND_TIME}
|
||||
duration={'date_and_time'}
|
||||
expiryTime={expiresAt}
|
||||
handleItemClick={this.handleItemClick}
|
||||
isSelected={duration === CustomStatusDuration.DATE_AND_TIME && expiresAt === ''}
|
||||
isSelected={duration === 'date_and_time' && expiresAt === ''}
|
||||
separator={false}
|
||||
showDateTimePicker={duration === CustomStatusDuration.DATE_AND_TIME}
|
||||
showDateTimePicker={duration === 'date_and_time'}
|
||||
showExpiryTime={showExpiryTime}
|
||||
/>
|
||||
</View>
|
||||
|
||||
213
app/utils/graphql.ts
Normal file
213
app/utils/graphql.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
const defaultNotifyProps: UserNotifyProps = {
|
||||
channel: 'true',
|
||||
comments: 'never',
|
||||
desktop: 'mention',
|
||||
desktop_sound: 'true',
|
||||
email: 'true',
|
||||
email_threads: 'all',
|
||||
first_name: 'false',
|
||||
mention_keys: '',
|
||||
push: 'mention',
|
||||
push_status: 'away',
|
||||
push_threads: 'all',
|
||||
|
||||
};
|
||||
export const gqlToClientUser = (u: Partial<GQLUser>): UserProfile => {
|
||||
return {
|
||||
id: u.id || '',
|
||||
create_at: u.createAt || 0,
|
||||
update_at: u.updateAt || 0,
|
||||
delete_at: u.deleteAt || 0,
|
||||
username: u.username || '',
|
||||
auth_service: u.authService || '',
|
||||
|
||||
email: u.email || '',
|
||||
email_verified: u.emailVerified ?? true,
|
||||
nickname: u.nickname || '',
|
||||
first_name: u.firstName || '',
|
||||
last_name: u.lastName || '',
|
||||
position: u.position || '',
|
||||
roles: u.roles?.map((v) => v.name!).join(' ') || '',
|
||||
locale: u.locale || '',
|
||||
notify_props: u.notifyProps || defaultNotifyProps,
|
||||
props: u.props,
|
||||
|
||||
timezone: u.timezone,
|
||||
is_bot: u.isBot,
|
||||
last_picture_update: u.lastPictureUpdate,
|
||||
remote_id: u.remoteId,
|
||||
status: u.status?.status || '',
|
||||
bot_description: u.botDescription,
|
||||
bot_last_icon_update: u.botLastIconUpdate,
|
||||
|
||||
auth_data: '',
|
||||
terms_of_service_id: '',
|
||||
terms_of_service_create_at: 0,
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
export const gqlToClientSession = (s: Partial<GQLSession>): Session => {
|
||||
return {
|
||||
create_at: s.createAt || 0,
|
||||
expires_at: s.expiresAt || 0,
|
||||
id: '',
|
||||
user_id: '',
|
||||
};
|
||||
};
|
||||
|
||||
export const gqlToClientTeamMembership = (m: Partial<GQLTeamMembership>, userId?: string): TeamMembership => {
|
||||
return {
|
||||
team_id: m.team?.id || '',
|
||||
delete_at: m.deleteAt || 0,
|
||||
roles: m.roles?.map((v) => v.name!).join(' ') || '',
|
||||
user_id: m.user?.id || userId || '',
|
||||
scheme_admin: m.schemeAdmin || false,
|
||||
scheme_user: m.schemeUser || false,
|
||||
mention_count: 0,
|
||||
msg_count: 0,
|
||||
};
|
||||
};
|
||||
|
||||
export const gqlToClientSidebarCategory = (c: Partial<GQLSidebarCategory>, teamId: string): CategoryWithChannels => {
|
||||
return {
|
||||
channel_ids: c.channelIds || [],
|
||||
collapsed: c.collapsed || false,
|
||||
display_name: c.displayName || '',
|
||||
id: c.id || '',
|
||||
muted: c.muted || false,
|
||||
sort_order: c.sortOrder || 0,
|
||||
sorting: c.sorting || 'alpha',
|
||||
team_id: c.teamId || teamId,
|
||||
type: c.type || 'channels',
|
||||
};
|
||||
};
|
||||
|
||||
export const gqlToClientTeam = (t: Partial<GQLTeam>): Team => {
|
||||
return {
|
||||
allow_open_invite: t.allowOpenInvite || false,
|
||||
allowed_domains: t.allowedDomains || '',
|
||||
company_name: t.companyName || '',
|
||||
create_at: t.createAt || 0,
|
||||
delete_at: t.deleteAt || 0,
|
||||
description: t.description || '',
|
||||
display_name: t.displayName || '',
|
||||
email: t.email || '',
|
||||
group_constrained: t.groupConstrained || false,
|
||||
id: t.id || '',
|
||||
invite_id: t.inviteId || '',
|
||||
last_team_icon_update: t.lastTeamIconUpdate || 0,
|
||||
name: t.name || '',
|
||||
scheme_id: t.schemeId || '',
|
||||
type: t.type || 'I',
|
||||
update_at: t.updateAt || 0,
|
||||
|
||||
// cloudLimitsArchived and policyId not used
|
||||
};
|
||||
};
|
||||
|
||||
export const gqlToClientPreference = (p: Partial<GQLPreference>): PreferenceType => {
|
||||
return {
|
||||
category: p.category || '',
|
||||
name: p.name || '',
|
||||
user_id: p.userId || '',
|
||||
value: p.value || '',
|
||||
};
|
||||
};
|
||||
|
||||
export const gqlToClientChannelMembership = (m: Partial<GQLChannelMembership>, userId?: string): ChannelMembership => {
|
||||
return {
|
||||
channel_id: m.channel?.id || '',
|
||||
last_update_at: m.lastUpdateAt || 0,
|
||||
last_viewed_at: m.lastViewedAt || 0,
|
||||
mention_count: m.mentionCount || 0,
|
||||
msg_count: m.msgCount || 0,
|
||||
msg_count_root: m.msgCountRoot || 0,
|
||||
notify_props: m.notifyProps || {},
|
||||
roles: m.roles?.map((r) => r.name).join(' ') || '',
|
||||
user_id: m.user?.id || userId || '',
|
||||
is_unread: false,
|
||||
last_post_at: 0,
|
||||
post_root_id: '',
|
||||
scheme_admin: m.schemeAdmin,
|
||||
scheme_user: m.schemeUser,
|
||||
};
|
||||
};
|
||||
|
||||
export const gqlToClientChannel = (c: Partial<GQLChannel>, teamId?: string): Channel => {
|
||||
return {
|
||||
create_at: c.createAt || 0,
|
||||
creator_id: c.creatorId || '',
|
||||
delete_at: c.deleteAt || 0,
|
||||
display_name: c.prettyDisplayName || c.displayName || '',
|
||||
extra_update_at: 0,
|
||||
group_constrained: c.groupConstrained || false,
|
||||
header: c.header || '',
|
||||
id: c.id || '',
|
||||
last_post_at: c.lastPostAt || 0,
|
||||
last_root_post_at: c.lastRootPostAt || 0,
|
||||
name: c.name || '',
|
||||
purpose: c.purpose || '',
|
||||
scheme_id: c.schemeId || '',
|
||||
shared: c.shared || false,
|
||||
team_id: c.team?.id || teamId || '',
|
||||
total_msg_count: c.totalMsgCount || 0,
|
||||
total_msg_count_root: c.totalMsgCountRoot || 0,
|
||||
type: c.type || 'O',
|
||||
update_at: c.updateAt || 0,
|
||||
fake: false,
|
||||
isCurrent: false,
|
||||
status: '',
|
||||
teammate_id: '',
|
||||
};
|
||||
};
|
||||
|
||||
export const gqlToClientChannelStats = (s: Partial<GQLChannel>): ChannelStats => {
|
||||
return {
|
||||
channel_id: s.id || '',
|
||||
guest_count: s.stats?.guestCount || 0,
|
||||
member_count: s.stats?.memberCount || 0,
|
||||
pinnedpost_count: s.stats?.pinnePostCount || 0,
|
||||
};
|
||||
};
|
||||
|
||||
export const gqlToClientRole = (r: Partial<GQLRole>): Role => {
|
||||
return {
|
||||
id: r.id || '',
|
||||
name: r.name || '',
|
||||
permissions: r.permissions || [],
|
||||
built_in: r.builtIn,
|
||||
scheme_managed: r.schemeManaged,
|
||||
};
|
||||
};
|
||||
|
||||
export const getMemberChannelsFromGQLQuery = (data: GQLData) => {
|
||||
return data.channelMembers?.reduce<Channel[]>((acc, m) => {
|
||||
if (m.channel) {
|
||||
acc.push(gqlToClientChannel(m.channel));
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const getMemberTeamsFromGQLQuery = (data: GQLData) => {
|
||||
return data.teamMembers?.reduce<Team[]>((acc, m) => {
|
||||
if (m.team) {
|
||||
acc.push(gqlToClientTeam(m.team));
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const filterAndTransformRoles = (roles: Array<Partial<GQLRole> | undefined>) => {
|
||||
const byName = roles.reduce<{[name: string]: Partial<GQLRole>}>((acum, r) => {
|
||||
if (r?.name && !acum[r.name]) {
|
||||
acum[r.name] = r;
|
||||
}
|
||||
return acum;
|
||||
}, {});
|
||||
return Object.values(byName).map((r) => gqlToClientRole(r));
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import moment from 'moment-timezone';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import {General, Permissions, Preferences} from '@constants';
|
||||
import {CustomStatusDuration} from '@constants/custom_status';
|
||||
import {CustomStatusDurationEnum} from '@constants/custom_status';
|
||||
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
|
||||
import {toTitleCase} from '@utils/helpers';
|
||||
|
||||
@@ -172,7 +172,7 @@ export function isCustomStatusExpired(user?: UserModel | UserProfile) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (customStatus.duration === CustomStatusDuration.DONT_CLEAR || !customStatus.duration) {
|
||||
if (customStatus.duration === CustomStatusDurationEnum.DONT_CLEAR || !customStatus.duration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ export function confirmOutOfOfficeDisabled(intl: IntlShape, status: string, upda
|
||||
}
|
||||
|
||||
export function isBot(user: UserProfile | UserModel): boolean {
|
||||
return 'is_bot' in user ? Boolean(user.is_bot) : Boolean(user.isBot);
|
||||
return 'isBot' in user ? Boolean(user.isBot) : Boolean(user.is_bot);
|
||||
}
|
||||
|
||||
export function isShared(user: UserProfile | UserModel): boolean {
|
||||
|
||||
1
types/api/config.d.ts
vendored
1
types/api/config.d.ts
vendored
@@ -118,6 +118,7 @@ interface ClientConfig {
|
||||
ExtendSessionLengthWithActivity: string;
|
||||
FeatureFlagAppsEnabled?: string;
|
||||
FeatureFlagCollapsedThreads?: string;
|
||||
FeatureFlagGraphQL?: string;
|
||||
GfycatApiKey: string;
|
||||
GfycatApiSecret: string;
|
||||
GoogleDeveloperKey: string;
|
||||
|
||||
183
types/api/graphql.d.ts
vendored
Normal file
183
types/api/graphql.d.ts
vendored
Normal file
@@ -0,0 +1,183 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
type GQLResponse = {
|
||||
errors?: GQLError[];
|
||||
data: GQLData;
|
||||
}
|
||||
|
||||
type GQLData = {
|
||||
user?: Partial<GQLUser>;
|
||||
config?: ClientConfig;
|
||||
license?: ClientLicense;
|
||||
teamMembers: Array<Partial<GQLTeamMembership>>;
|
||||
channels?: Array<Partial<GQLChannel>>;
|
||||
channelsLeft?: Array<Partial<GQLChannel>>;
|
||||
channelMembers?: Array<Partial<GQLChannelMembership>>;
|
||||
sidebarCategories?: Array<Partial<GQLSidebarCategory>>;
|
||||
}
|
||||
|
||||
type GQLError = {
|
||||
message: string;
|
||||
path: Array<string | number>;
|
||||
}
|
||||
|
||||
type GQLUser = {
|
||||
id: string;
|
||||
createAt: number;
|
||||
updateAt: number;
|
||||
deleteAt: number;
|
||||
username: string;
|
||||
authService: string;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
nickname: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
position: string;
|
||||
locale: string;
|
||||
notifyProps: UserNotifyProps;
|
||||
props: UserProps;
|
||||
timezone: UserTimezone;
|
||||
isBot: boolean;
|
||||
lastPictureUpdate: number;
|
||||
remoteId: string;
|
||||
botDescription: string;
|
||||
botLastIconUpdate: number;
|
||||
|
||||
roles: Array<Partial<GQLRole>>;
|
||||
customStatus: Partial<GQLUserCustomStatus>;
|
||||
status: Partial<GQLUserStatus>;
|
||||
preferences: Array<Partial<GQLPreference>>;
|
||||
sessions: Array<Partial<GQLSession>>;
|
||||
|
||||
// Derived
|
||||
isSystemAdmin: boolean;
|
||||
isGuest: boolean;
|
||||
}
|
||||
|
||||
type GQLSession = {
|
||||
createAt: number;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
type GQLTeamMembership = {
|
||||
team: Partial<GQLTeam>;
|
||||
user: Partial<GQLUser>;
|
||||
roles: Array<Partial<GQLRole>>;
|
||||
deleteAt: number;
|
||||
schemeGuest: boolean;
|
||||
schemeUser: boolean;
|
||||
schemeAdmin: boolean;
|
||||
}
|
||||
|
||||
type GQLSidebarCategory = {
|
||||
id: string;
|
||||
sorting: CategorySorting;
|
||||
type: CategoryType;
|
||||
displayName: string;
|
||||
muted: boolean;
|
||||
collapsed: boolean;
|
||||
channelIds: string[];
|
||||
sortOrder: number;
|
||||
teamId: string;
|
||||
}
|
||||
|
||||
type GQLTeam = {
|
||||
id: string;
|
||||
displayName: string;
|
||||
name: string;
|
||||
description: string;
|
||||
email: string;
|
||||
type: TeamType;
|
||||
companyName: string;
|
||||
allowedDomains: string;
|
||||
inviteId: string;
|
||||
lastTeamIconUpdate: number;
|
||||
groupConstrained: boolean;
|
||||
allowOpenInvite: boolean;
|
||||
updateAt: number;
|
||||
createAt: number;
|
||||
deleteAt: number;
|
||||
schemeId: string;
|
||||
policyId: string;
|
||||
cloudLimitsArchived: boolean;
|
||||
}
|
||||
|
||||
type GQLUserCustomStatus = {
|
||||
emoji: string;
|
||||
text: string;
|
||||
duration: CustomStatusDuration;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
type GQLUserStatus = {
|
||||
status: string;
|
||||
manual: boolean;
|
||||
lastActivityAt: number;
|
||||
activeChannel: string;
|
||||
dndEndTime: number;
|
||||
}
|
||||
|
||||
type GQLPreference = {
|
||||
userId: string;
|
||||
category: string;
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
type GQLChannelMembership = {
|
||||
channel: Partial<GQLChannel>;
|
||||
user: Partial<GQLUser>;
|
||||
roles: Array<Partial<GQLRole>>;
|
||||
lastViewedAt: number;
|
||||
msgCount: number;
|
||||
msgCountRoot: number;
|
||||
mentionCount: number;
|
||||
mentionCountRoot: number;
|
||||
notifyProps: ChannelNotifyProps;
|
||||
lastUpdateAt: number;
|
||||
schemeGuest: boolean;
|
||||
schemeUser: boolean;
|
||||
schemeAdmin: boolean;
|
||||
explicitRoles: string;
|
||||
cursor: string;
|
||||
}
|
||||
|
||||
type GQLChannel = {
|
||||
id: string;
|
||||
createAt: number;
|
||||
updateAt: number;
|
||||
deleteAt: number;
|
||||
type: ChannelType;
|
||||
displayName: string;
|
||||
prettyDisplayName: string;
|
||||
name: string;
|
||||
header: string;
|
||||
purpose: string;
|
||||
creatorId: string;
|
||||
schemeId: string;
|
||||
team: Partial<GQLTeam>;
|
||||
cursor: string;
|
||||
groupConstrained: boolean;
|
||||
shared: boolean;
|
||||
lastPostAt: number;
|
||||
lastRootPostAt: number;
|
||||
totalMsgCount: number;
|
||||
totalMsgCountRoot: number;
|
||||
stats: Partial<GQLStats>;
|
||||
}
|
||||
|
||||
type GQLStats = {
|
||||
guestCount: number;
|
||||
memberCount: number;
|
||||
pinnePostCount: number;
|
||||
}
|
||||
|
||||
type GQLRole = {
|
||||
id: string;
|
||||
name: string;
|
||||
permissions: string[];
|
||||
schemeManaged: boolean;
|
||||
builtIn: boolean;
|
||||
}
|
||||
8
types/api/users.d.ts
vendored
8
types/api/users.d.ts
vendored
@@ -11,7 +11,7 @@ type UserNotifyProps = {
|
||||
desktop_sound: 'true' | 'false';
|
||||
email: 'true' | 'false';
|
||||
first_name: 'true' | 'false';
|
||||
mark_unread: 'all' | 'mention';
|
||||
mark_unread?: 'all' | 'mention';
|
||||
mention_keys: string;
|
||||
push: 'default' | 'all' | 'mention' | 'none';
|
||||
push_status: 'ooo' | 'offline' | 'away' | 'dnd' | 'online';
|
||||
@@ -41,8 +41,8 @@ type UserProfile = {
|
||||
terms_of_service_id?: string;
|
||||
terms_of_service_create_at?: number;
|
||||
timezone?: UserTimezone;
|
||||
is_bot: boolean;
|
||||
last_picture_update: number;
|
||||
is_bot?: boolean;
|
||||
last_picture_update?: number;
|
||||
remote_id?: string;
|
||||
status?: string;
|
||||
bot_description?: string;
|
||||
@@ -96,3 +96,5 @@ type UserCustomStatus = {
|
||||
expires_at?: string;
|
||||
duration?: CustomStatusDuration;
|
||||
};
|
||||
|
||||
type CustomStatusDuration = '' | 'thirty_minutes' | 'one_hour' | 'four_hours' | 'today' | 'this_week' | 'date_and_time';
|
||||
|
||||
Reference in New Issue
Block a user