forked from Ivasoft/mattermost-mobile
Fix several issues around team join (#6863)
* Fix several issues around team join * Open in modal and fix channel list * Add joining states and fix issues * i18n-extract * add specific message for group related failures on joining teams * Address feedback * Address feedback * Use error from server response
This commit is contained in:
committed by
GitHub
parent
4e3531fb52
commit
b1e4403768
@@ -9,7 +9,7 @@ import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||
import {MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference';
|
||||
import {fetchRoles} from '@actions/remote/role';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import {fetchAllTeams, fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, handleKickFromTeam, MyTeamsRequest} from '@actions/remote/team';
|
||||
import {fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, handleKickFromTeam, MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
|
||||
import {syncTeamThreads} from '@actions/remote/thread';
|
||||
import {autoUpdateTimezone, fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {gqlAllChannels} from '@client/graphQL/entry';
|
||||
@@ -343,7 +343,7 @@ export async function restDeferredAppEntryActions(
|
||||
}
|
||||
}
|
||||
|
||||
await fetchAllTeams(serverUrl);
|
||||
updateCanJoinTeams(serverUrl);
|
||||
await updateAllUsersSince(serverUrl, since);
|
||||
|
||||
// Fetch groups for current user
|
||||
|
||||
@@ -7,7 +7,7 @@ import {storeConfigAndLicense} from '@actions/local/systems';
|
||||
import {MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {fetchGroupsForMember} from '@actions/remote/groups';
|
||||
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||
import {MyTeamsRequest} from '@actions/remote/team';
|
||||
import {MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
|
||||
import {syncTeamThreads} from '@actions/remote/thread';
|
||||
import {autoUpdateTimezone, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {gqlEntry, gqlEntryChannels, gqlOtherChannels} from '@client/graphQL/entry';
|
||||
@@ -95,6 +95,7 @@ export async function deferredAppEntryGraphQLActions(
|
||||
// Fetch groups for current user
|
||||
fetchGroupsForMember(serverUrl, currentUserId);
|
||||
|
||||
updateCanJoinTeams(serverUrl);
|
||||
updateAllUsersSince(serverUrl, since);
|
||||
|
||||
return {error: undefined};
|
||||
@@ -180,9 +181,22 @@ export const entryGQL = async (serverUrl: string, currentTeamId?: string, curren
|
||||
user: gqlToClientUser(fetchedData.user!),
|
||||
};
|
||||
|
||||
const allTeams = getMemberTeamsFromGQLQuery(fetchedData);
|
||||
const allTeamMemberships = fetchedData.teamMembers.map((m) => gqlToClientTeamMembership(m, meData.user.id));
|
||||
|
||||
const [nonArchivedTeams, archivedTeamIds] = allTeams.reduce((acc, t) => {
|
||||
if (t.delete_at) {
|
||||
acc[1].add(t.id);
|
||||
return acc;
|
||||
}
|
||||
return [[...acc[0], t], acc[1]];
|
||||
}, [[], new Set<string>()]);
|
||||
|
||||
const nonArchivedTeamMemberships = allTeamMemberships.filter((m) => !archivedTeamIds.has(m.team_id));
|
||||
|
||||
const teamData = {
|
||||
teams: getMemberTeamsFromGQLQuery(fetchedData),
|
||||
memberships: fetchedData.teamMembers.map((m) => gqlToClientTeamMembership(m, meData.user.id)),
|
||||
teams: nonArchivedTeams,
|
||||
memberships: nonArchivedTeamMemberships,
|
||||
};
|
||||
|
||||
const prefData = {
|
||||
@@ -202,7 +216,7 @@ export const entryGQL = async (serverUrl: string, currentTeamId?: string, curren
|
||||
let initialTeamId = currentTeamId;
|
||||
if (!teamData.teams.length) {
|
||||
initialTeamId = '';
|
||||
} else if (!initialTeamId || !teamData.teams.find((t) => t.id === currentTeamId)) {
|
||||
} else if (!initialTeamId || !teamData.teams.find((t) => t.id === currentTeamId && t.delete_at === 0)) {
|
||||
const teamOrderPreference = getPreferenceValue(prefData.preferences || [], Preferences.TEAMS_ORDER, '', '') as string;
|
||||
initialTeamId = selectDefaultTeam(teamData.teams, meData.user.locale, teamOrderPreference, config.ExperimentalPrimaryTeam)?.id || '';
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import {getIsCRTEnabled, prepareThreadsFromReceivedPosts} from '@queries/servers
|
||||
import {queryAllUsers} from '@queries/servers/user';
|
||||
import {setFetchingThreadState} from '@store/fetching_thread_store';
|
||||
import {getValidEmojis, matchEmoticons} from '@utils/emoji/helpers';
|
||||
import {isServerError} from '@utils/errors';
|
||||
import {logError} from '@utils/log';
|
||||
import {processPostsFetched} from '@utils/post';
|
||||
import {getPostIdsForCombinedUserActivityPost} from '@utils/post_list';
|
||||
@@ -134,7 +135,7 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
|
||||
let created;
|
||||
try {
|
||||
created = await client.createPost(newPost);
|
||||
} catch (error: any) {
|
||||
} catch (error) {
|
||||
const errorPost = {
|
||||
...newPost,
|
||||
id: pendingPostId,
|
||||
@@ -147,10 +148,11 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
|
||||
|
||||
// If the failure was because: the root post was deleted or
|
||||
// TownSquareIsReadOnly=true then remove the post
|
||||
if (error.server_error_id === ServerErrors.DELETED_ROOT_POST_ERROR ||
|
||||
if (isServerError(error) && (
|
||||
error.server_error_id === ServerErrors.DELETED_ROOT_POST_ERROR ||
|
||||
error.server_error_id === ServerErrors.TOWN_SQUARE_READ_ONLY_ERROR ||
|
||||
error.server_error_id === ServerErrors.PLUGIN_DISMISSED_POST_ERROR
|
||||
) {
|
||||
)) {
|
||||
await removePost(serverUrl, databasePost);
|
||||
} else {
|
||||
const models = await operator.handlePosts({
|
||||
@@ -252,11 +254,12 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
|
||||
}
|
||||
}
|
||||
await operator.batchRecords(models);
|
||||
} catch (error: any) {
|
||||
if (error.server_error_id === ServerErrors.DELETED_ROOT_POST_ERROR ||
|
||||
} catch (error) {
|
||||
if (isServerError(error) && (
|
||||
error.server_error_id === ServerErrors.DELETED_ROOT_POST_ERROR ||
|
||||
error.server_error_id === ServerErrors.TOWN_SQUARE_READ_ONLY_ERROR ||
|
||||
error.server_error_id === ServerErrors.PLUGIN_DISMISSED_POST_ERROR
|
||||
) {
|
||||
)) {
|
||||
await removePost(serverUrl, post);
|
||||
} else {
|
||||
post.prepareUpdate((p) => {
|
||||
|
||||
@@ -5,6 +5,8 @@ import {Model} from '@nozbe/watermelondb';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {removeUserFromTeam as localRemoveUserFromTeam} from '@actions/local/team';
|
||||
import {Client} from '@client/rest';
|
||||
import {PER_PAGE_DEFAULT} from '@client/rest/constants';
|
||||
import {Events} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
@@ -12,8 +14,8 @@ import {getActiveServerUrl} from '@queries/app/servers';
|
||||
import {prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
|
||||
import {prepareMyChannelsForTeam, getDefaultChannelForTeam} from '@queries/servers/channel';
|
||||
import {prepareCommonSystemValues, getCurrentTeamId, getCurrentUserId} from '@queries/servers/system';
|
||||
import {addTeamToTeamHistory, prepareDeleteTeam, prepareMyTeams, getNthLastChannelFromTeam, queryTeamsById, syncTeamTable, getLastTeam, getTeamById, removeTeamFromTeamHistory} from '@queries/servers/team';
|
||||
import {dismissAllModals, popToRoot, resetToTeams} from '@screens/navigation';
|
||||
import {addTeamToTeamHistory, prepareDeleteTeam, prepareMyTeams, getNthLastChannelFromTeam, queryTeamsById, getLastTeam, getTeamById, removeTeamFromTeamHistory, queryMyTeams} from '@queries/servers/team';
|
||||
import {dismissAllModals, popToRoot} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
@@ -100,6 +102,7 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s
|
||||
}
|
||||
}
|
||||
EphemeralStore.finishAddingToTeam(teamId);
|
||||
updateCanJoinTeams(serverUrl);
|
||||
return {member};
|
||||
} catch (error) {
|
||||
if (loadEventSent) {
|
||||
@@ -195,23 +198,10 @@ export async function fetchMyTeam(serverUrl: string, teamId: string, fetchOnly =
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchAllTeams = async (serverUrl: string, fetchOnly = false): Promise<MyTeamsRequest> => {
|
||||
let client;
|
||||
export const fetchAllTeams = async (serverUrl: string, page = 0, perPage = PER_PAGE_DEFAULT): Promise<{teams?: Team[]; error?: any}> => {
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const teams = await client.getTeams();
|
||||
if (!fetchOnly) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (operator) {
|
||||
syncTeamTable(operator, teams);
|
||||
}
|
||||
}
|
||||
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const teams = await client.getTeams(page, perPage);
|
||||
return {teams};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
@@ -219,6 +209,76 @@ export const fetchAllTeams = async (serverUrl: string, fetchOnly = false): Promi
|
||||
}
|
||||
};
|
||||
|
||||
const recCanJoinTeams = async (client: Client, myTeamsIds: Set<string>, page: number): Promise<boolean> => {
|
||||
const fetchedTeams = await client.getTeams(page, PER_PAGE_DEFAULT);
|
||||
if (fetchedTeams.find((t) => !myTeamsIds.has(t.id) && t.delete_at === 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (fetchedTeams.length === PER_PAGE_DEFAULT) {
|
||||
return recCanJoinTeams(client, myTeamsIds, page + 1);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const LOAD_MORE_THRESHOLD = 10;
|
||||
export async function fetchTeamsForComponent(
|
||||
serverUrl: string,
|
||||
page: number,
|
||||
joinedIds?: Set<string>,
|
||||
alreadyLoaded: Team[] = [],
|
||||
): Promise<{teams: Team[]; hasMore: boolean; page: number}> {
|
||||
let hasMore = true;
|
||||
const {teams, error} = await fetchAllTeams(serverUrl, page, PER_PAGE_DEFAULT);
|
||||
if (error || !teams || teams.length < PER_PAGE_DEFAULT) {
|
||||
hasMore = false;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return {teams: alreadyLoaded, hasMore, page};
|
||||
}
|
||||
|
||||
if (teams?.length) {
|
||||
const notJoinedTeams = joinedIds ? teams.filter((t) => !joinedIds.has(t.id)) : teams;
|
||||
alreadyLoaded.push(...notJoinedTeams);
|
||||
|
||||
if (teams.length < PER_PAGE_DEFAULT) {
|
||||
hasMore = false;
|
||||
}
|
||||
|
||||
if (
|
||||
hasMore &&
|
||||
(alreadyLoaded.length > LOAD_MORE_THRESHOLD)
|
||||
) {
|
||||
return fetchTeamsForComponent(serverUrl, page + 1, joinedIds, alreadyLoaded);
|
||||
}
|
||||
|
||||
return {teams: alreadyLoaded, hasMore, page: page + 1};
|
||||
}
|
||||
|
||||
return {teams: alreadyLoaded, hasMore: false, page};
|
||||
}
|
||||
|
||||
export const updateCanJoinTeams = async (serverUrl: string) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const myTeams = await queryMyTeams(database).fetch();
|
||||
const myTeamsIds = new Set(myTeams.map((m) => m.id));
|
||||
|
||||
const canJoin = await recCanJoinTeams(client, myTeamsIds, 0);
|
||||
|
||||
EphemeralStore.setCanJoinOtherTeams(serverUrl, canJoin);
|
||||
return {};
|
||||
} catch (error) {
|
||||
EphemeralStore.setCanJoinOtherTeams(serverUrl, false);
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchTeamsChannelsAndUnreadPosts = async (serverUrl: string, since: number, teams: Team[], memberships: TeamMembership[], excludeTeamId?: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
@@ -288,7 +348,7 @@ export const removeUserFromTeam = async (serverUrl: string, teamId: string, user
|
||||
|
||||
if (!fetchOnly) {
|
||||
localRemoveUserFromTeam(serverUrl, teamId);
|
||||
fetchAllTeams(serverUrl);
|
||||
updateCanJoinTeams(serverUrl);
|
||||
}
|
||||
|
||||
return {error: undefined};
|
||||
@@ -361,9 +421,9 @@ export async function handleKickFromTeam(serverUrl: string, teamId: string) {
|
||||
const teamToJumpTo = await getLastTeam(database, teamId);
|
||||
if (teamToJumpTo) {
|
||||
await handleTeamChange(serverUrl, teamToJumpTo);
|
||||
} else if (currentServer === serverUrl) {
|
||||
await resetToTeams();
|
||||
}
|
||||
|
||||
// Resetting to team select handled by the home screen
|
||||
} catch (error) {
|
||||
logDebug('Failed to kick user from team', error);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ import {handlePreferenceChangedEvent, handlePreferencesChangedEvent, handlePrefe
|
||||
import {handleAddCustomEmoji, handleReactionRemovedFromPostEvent, handleReactionAddedToPostEvent} from './reactions';
|
||||
import {handleUserRoleUpdatedEvent, handleTeamMemberRoleUpdatedEvent, handleRoleUpdatedEvent} from './roles';
|
||||
import {handleLicenseChangedEvent, handleConfigChangedEvent} from './system';
|
||||
import {handleLeaveTeamEvent, handleUserAddedToTeamEvent, handleUpdateTeamEvent} from './teams';
|
||||
import {handleLeaveTeamEvent, handleUserAddedToTeamEvent, handleUpdateTeamEvent, handleTeamArchived, handleTeamRestored} from './teams';
|
||||
import {handleThreadUpdatedEvent, handleThreadReadChangedEvent, handleThreadFollowChangedEvent} from './threads';
|
||||
import {handleUserUpdatedEvent, handleUserTypingEvent} from './users';
|
||||
|
||||
@@ -312,6 +312,14 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
handleOpenDialogEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.DELETE_TEAM:
|
||||
handleTeamArchived(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.RESTORE_TEAM:
|
||||
handleTeamRestored(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.THREAD_UPDATED:
|
||||
handleThreadUpdatedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
@@ -6,17 +6,77 @@ import {Model} from '@nozbe/watermelondb';
|
||||
import {removeUserFromTeam} from '@actions/local/team';
|
||||
import {fetchMyChannelsForTeam} from '@actions/remote/channel';
|
||||
import {fetchRoles} from '@actions/remote/role';
|
||||
import {fetchAllTeams, fetchMyTeam, handleKickFromTeam} from '@actions/remote/team';
|
||||
import {fetchMyTeam, handleKickFromTeam, updateCanJoinTeams} from '@actions/remote/team';
|
||||
import {updateUsersNoLongerVisible} from '@actions/remote/user';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
|
||||
import {prepareMyChannelsForTeam} from '@queries/servers/channel';
|
||||
import {getCurrentTeam, prepareMyTeams} from '@queries/servers/team';
|
||||
import {getCurrentTeam, prepareMyTeams, queryMyTeamsByIds} from '@queries/servers/team';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
export async function handleTeamArchived(serverUrl: string, msg: WebSocketMessage) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const team: Team = JSON.parse(msg.data.team);
|
||||
|
||||
const membership = (await queryMyTeamsByIds(database, [team.id]).fetch())[0];
|
||||
if (membership) {
|
||||
const currentTeam = await getCurrentTeam(database);
|
||||
if (currentTeam?.id === team.id) {
|
||||
await handleKickFromTeam(serverUrl, team.id);
|
||||
}
|
||||
|
||||
await removeUserFromTeam(serverUrl, team.id);
|
||||
|
||||
const user = await getCurrentUser(database);
|
||||
if (user?.isGuest) {
|
||||
updateUsersNoLongerVisible(serverUrl);
|
||||
}
|
||||
}
|
||||
updateCanJoinTeams(serverUrl);
|
||||
} catch (error) {
|
||||
logDebug('cannot handle archive team websocket event', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleTeamRestored(serverUrl: string, msg: WebSocketMessage) {
|
||||
let markedAsLoading = false;
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const team: Team = JSON.parse(msg.data.team);
|
||||
|
||||
const teamMembership = await client.getTeamMember(team.id, 'me');
|
||||
if (teamMembership && teamMembership.delete_at === 0) {
|
||||
// Ignore duplicated team join events sent by the server
|
||||
if (EphemeralStore.isAddingToTeam(team.id)) {
|
||||
return;
|
||||
}
|
||||
EphemeralStore.startAddingToTeam(team.id);
|
||||
|
||||
setTeamLoading(serverUrl, true);
|
||||
markedAsLoading = true;
|
||||
await fetchAndStoreJoinedTeamInfo(serverUrl, operator, team.id, [team], [teamMembership]);
|
||||
setTeamLoading(serverUrl, false);
|
||||
markedAsLoading = false;
|
||||
|
||||
EphemeralStore.finishAddingToTeam(team.id);
|
||||
}
|
||||
|
||||
updateCanJoinTeams(serverUrl);
|
||||
} catch (error) {
|
||||
if (markedAsLoading) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
}
|
||||
logDebug('cannot handle restore team websocket event', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleLeaveTeamEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
@@ -34,7 +94,7 @@ export async function handleLeaveTeamEvent(serverUrl: string, msg: WebSocketMess
|
||||
}
|
||||
|
||||
await removeUserFromTeam(serverUrl, teamId);
|
||||
fetchAllTeams(serverUrl);
|
||||
updateCanJoinTeams(serverUrl);
|
||||
|
||||
if (user.isGuest) {
|
||||
updateUsersNoLongerVisible(serverUrl);
|
||||
@@ -76,27 +136,31 @@ export async function handleUserAddedToTeamEvent(serverUrl: string, msg: WebSock
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const {teams, memberships: teamMemberships} = await fetchMyTeam(serverUrl, teamId, true);
|
||||
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
if (teams?.length && teamMemberships?.length) {
|
||||
const {channels, memberships, categories} = await fetchMyChannelsForTeam(serverUrl, teamId, false, 0, true);
|
||||
modelPromises.push(prepareCategoriesAndCategoriesChannels(operator, categories || [], true));
|
||||
modelPromises.push(...await prepareMyChannelsForTeam(operator, teamId, channels || [], memberships || []));
|
||||
|
||||
const {roles} = await fetchRoles(serverUrl, teamMemberships, memberships, undefined, true);
|
||||
modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true}));
|
||||
}
|
||||
|
||||
if (teams && teamMemberships) {
|
||||
modelPromises.push(...prepareMyTeams(operator, teams, teamMemberships));
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
setTeamLoading(serverUrl, false);
|
||||
await fetchAndStoreJoinedTeamInfo(serverUrl, operator, teamId, teams, teamMemberships);
|
||||
} catch (error) {
|
||||
logDebug('could not handle user added to team websocket event');
|
||||
setTeamLoading(serverUrl, false);
|
||||
}
|
||||
|
||||
setTeamLoading(serverUrl, false);
|
||||
EphemeralStore.finishAddingToTeam(teamId);
|
||||
}
|
||||
|
||||
const fetchAndStoreJoinedTeamInfo = async (serverUrl: string, operator: ServerDataOperator, teamId: string, teams?: Team[], teamMemberships?: TeamMembership[]) => {
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
if (teams?.length && teamMemberships?.length) {
|
||||
const {channels, memberships, categories} = await fetchMyChannelsForTeam(serverUrl, teamId, false, 0, true);
|
||||
modelPromises.push(prepareCategoriesAndCategoriesChannels(operator, categories || [], true));
|
||||
modelPromises.push(...await prepareMyChannelsForTeam(operator, teamId, channels || [], memberships || []));
|
||||
|
||||
const {roles} = await fetchRoles(serverUrl, teamMemberships, memberships, undefined, true);
|
||||
if (roles?.length) {
|
||||
modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true}));
|
||||
}
|
||||
}
|
||||
|
||||
if (teams && teamMemberships) {
|
||||
modelPromises.push(...prepareMyTeams(operator, teams, teamMemberships));
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
};
|
||||
|
||||
@@ -5,6 +5,8 @@ import React, {useCallback} from 'react';
|
||||
import {ListRenderItemInfo, StyleSheet, View} from 'react-native';
|
||||
import {FlatList} from 'react-native-gesture-handler'; // Keep the FlatList from gesture handler so it works well with bottom sheet
|
||||
|
||||
import Loading from '@components/loading';
|
||||
|
||||
import TeamListItem from './team_list_item';
|
||||
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
@@ -17,6 +19,8 @@ type Props = {
|
||||
onPress: (id: string) => void;
|
||||
testID?: string;
|
||||
selectedTeamId?: string;
|
||||
onEndReached?: () => void;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -30,7 +34,7 @@ const styles = StyleSheet.create({
|
||||
|
||||
const keyExtractor = (item: TeamModel) => item.id;
|
||||
|
||||
export default function TeamList({teams, textColor, iconTextColor, iconBackgroundColor, onPress, testID, selectedTeamId}: Props) {
|
||||
export default function TeamList({teams, textColor, iconTextColor, iconBackgroundColor, onPress, testID, selectedTeamId, onEndReached, loading = false}: Props) {
|
||||
const renderTeam = useCallback(({item: t}: ListRenderItemInfo<Team|TeamModel>) => {
|
||||
return (
|
||||
<TeamListItem
|
||||
@@ -44,6 +48,11 @@ export default function TeamList({teams, textColor, iconTextColor, iconBackgroun
|
||||
);
|
||||
}, [textColor, iconTextColor, iconBackgroundColor, onPress, selectedTeamId]);
|
||||
|
||||
let footer;
|
||||
if (loading) {
|
||||
footer = (<Loading/>);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<FlatList
|
||||
@@ -52,6 +61,8 @@ export default function TeamList({teams, textColor, iconTextColor, iconBackgroun
|
||||
keyExtractor={keyExtractor}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
testID={`${testID}.flat_list`}
|
||||
onEndReached={onEndReached}
|
||||
ListFooterComponent={footer}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -1,105 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import {addCurrentUserToTeam, handleTeamChange} from '@actions/remote/team';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import Empty from '@components/illustrations/no_team';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import BottomSheetContent from '@screens/bottom_sheet/content';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import TeamList from './team_list';
|
||||
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
empty: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
lineHeight: 28,
|
||||
marginTop: 16,
|
||||
...typography('Heading', 400, 'Regular'),
|
||||
},
|
||||
description: {
|
||||
color: theme.centerChannelColor,
|
||||
marginTop: 8,
|
||||
maxWidth: 334,
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
otherTeams: TeamModel[];
|
||||
title: string;
|
||||
showTitle?: boolean;
|
||||
}
|
||||
|
||||
export default function AddTeamSlideUp({otherTeams, title, showTitle = true}: Props) {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const onPressCreate = useCallback(() => {
|
||||
//TODO Create team screen https://mattermost.atlassian.net/browse/MM-43622
|
||||
dismissBottomSheet();
|
||||
}, []);
|
||||
|
||||
const onPress = useCallback(async (teamId: string) => {
|
||||
const {error} = await addCurrentUserToTeam(serverUrl, teamId);
|
||||
if (!error) {
|
||||
await dismissBottomSheet();
|
||||
handleTeamChange(serverUrl, teamId);
|
||||
}
|
||||
}, [serverUrl]);
|
||||
|
||||
const hasOtherTeams = Boolean(otherTeams.length);
|
||||
|
||||
return (
|
||||
<BottomSheetContent
|
||||
buttonIcon='plus'
|
||||
buttonText={intl.formatMessage({id: 'mobile.add_team.create_team', defaultMessage: 'Create a new team'})}
|
||||
onPress={onPressCreate}
|
||||
showButton={false}
|
||||
showTitle={showTitle}
|
||||
testID='team_sidebar.add_team_slide_up'
|
||||
title={title}
|
||||
>
|
||||
{hasOtherTeams &&
|
||||
<TeamList
|
||||
teams={otherTeams}
|
||||
onPress={onPress}
|
||||
testID='team_sidebar.add_team_slide_up.team_list'
|
||||
/>
|
||||
}
|
||||
{!hasOtherTeams &&
|
||||
<View style={styles.empty}>
|
||||
<Empty theme={theme}/>
|
||||
<FormattedText
|
||||
id='team_list.no_other_teams.title'
|
||||
defaultMessage='No additional teams to join'
|
||||
style={styles.title}
|
||||
testID='team_sidebar.add_team_slide_up.no_other_teams.title'
|
||||
/>
|
||||
<FormattedText
|
||||
id='team_list.no_other_teams.description'
|
||||
defaultMessage='To join another team, ask a Team Admin for an invitation, or create your own team.'
|
||||
style={styles.description}
|
||||
testID='team_sidebar.add_team_slide_up.no_other_teams.description'
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
</BottomSheetContent>
|
||||
);
|
||||
}
|
||||
@@ -3,26 +3,16 @@
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useWindowDimensions, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {bottomSheet} from '@screens/navigation';
|
||||
import {showModal} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {getTeamsSnapHeight} from '@utils/team_list';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import AddTeamSlideUp from './add_team_slide_up';
|
||||
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
type Props = {
|
||||
otherTeams: TeamModel[];
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
@@ -45,35 +35,26 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
};
|
||||
});
|
||||
|
||||
export default function AddTeam({otherTeams}: Props) {
|
||||
export default function AddTeam() {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const dimensions = useWindowDimensions();
|
||||
const intl = useIntl();
|
||||
const insets = useSafeAreaInsets();
|
||||
const isTablet = useIsTablet();
|
||||
|
||||
const onPress = useCallback(preventDoubleTap(() => {
|
||||
const title = intl.formatMessage({id: 'mobile.add_team.join_team', defaultMessage: 'Join Another Team'});
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<AddTeamSlideUp
|
||||
otherTeams={otherTeams}
|
||||
showTitle={!isTablet && Boolean(otherTeams.length)}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
const closeButtonId = 'close-join-team';
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: closeButtonId,
|
||||
icon: closeButton,
|
||||
testID: 'close.join_team.button',
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
const height = getTeamsSnapHeight({dimensions, teams: otherTeams, insets});
|
||||
bottomSheet({
|
||||
closeButtonId: 'close-team_list',
|
||||
renderContent,
|
||||
snapPoints: [height, 10],
|
||||
theme,
|
||||
title,
|
||||
});
|
||||
}), [otherTeams, intl, isTablet, dimensions, theme]);
|
||||
showModal(Screens.JOIN_TEAM, title, {closeButtonId}, options);
|
||||
}), [intl]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
// 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 {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {queryMyTeams, queryOtherTeams} from '@queries/servers/team';
|
||||
import {withServerUrl} from '@context/server';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
|
||||
import TeamSidebar from './team_sidebar';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const enhanced = withObservables([], ({serverUrl}: {serverUrl: string}) => {
|
||||
// TODO https://mattermost.atlassian.net/browse/MM-43622
|
||||
// const canCreateTeams = observeCurrentUser(database).pipe(
|
||||
// switchMap((u) => (u ? of$(u.roles.split(' ')) : of$([]))),
|
||||
@@ -19,17 +16,9 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
// switchMap((r) => of$(hasPermission(r, Permissions.CREATE_TEAM, false))),
|
||||
// );
|
||||
|
||||
const otherTeams = queryMyTeams(database).observe().pipe(
|
||||
switchMap((mm) => {
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
const ids = mm.map((m) => m.id);
|
||||
return queryOtherTeams(database, ids).observe();
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
otherTeams,
|
||||
canJoinOtherTeams: EphemeralStore.observeCanJoinOtherTeams(serverUrl),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(TeamSidebar));
|
||||
export default withServerUrl(enhanced(TeamSidebar));
|
||||
|
||||
@@ -11,11 +11,9 @@ import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import AddTeam from './add_team';
|
||||
import TeamList from './team_list';
|
||||
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
type Props = {
|
||||
iconPad?: boolean;
|
||||
otherTeams: TeamModel[];
|
||||
canJoinOtherTeams: boolean;
|
||||
teamsCount: number;
|
||||
}
|
||||
|
||||
@@ -38,8 +36,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
};
|
||||
});
|
||||
|
||||
export default function TeamSidebar({iconPad, otherTeams, teamsCount}: Props) {
|
||||
const showAddTeam = otherTeams.length > 0;
|
||||
export default function TeamSidebar({iconPad, canJoinOtherTeams, teamsCount}: Props) {
|
||||
const initialWidth = teamsCount > 1 ? TEAM_SIDEBAR_WIDTH : 0;
|
||||
const width = useSharedValue(initialWidth);
|
||||
const marginTop = useSharedValue(iconPad ? 44 : 0);
|
||||
@@ -68,10 +65,8 @@ export default function TeamSidebar({iconPad, otherTeams, teamsCount}: Props) {
|
||||
<Animated.View style={[styles.container, transform]}>
|
||||
<Animated.View style={[styles.listContainer, serverStyle]}>
|
||||
<TeamList testID='team_sidebar.team_list'/>
|
||||
{showAddTeam && (
|
||||
<AddTeam
|
||||
otherTeams={otherTeams}
|
||||
/>
|
||||
{canJoinOtherTeams && (
|
||||
<AddTeam/>
|
||||
)}
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
|
||||
@@ -29,6 +29,7 @@ export const HOME = 'Home';
|
||||
export const INTEGRATION_SELECTOR = 'IntegrationSelector';
|
||||
export const INTERACTIVE_DIALOG = 'InteractiveDialog';
|
||||
export const IN_APP_NOTIFICATION = 'InAppNotification';
|
||||
export const JOIN_TEAM = 'JoinTeam';
|
||||
export const LATEX = 'Latex';
|
||||
export const LOGIN = 'Login';
|
||||
export const MENTIONS = 'Mentions';
|
||||
@@ -94,6 +95,7 @@ export default {
|
||||
INTEGRATION_SELECTOR,
|
||||
INTERACTIVE_DIALOG,
|
||||
IN_APP_NOTIFICATION,
|
||||
JOIN_TEAM,
|
||||
LATEX,
|
||||
LOGIN,
|
||||
MENTIONS,
|
||||
|
||||
@@ -5,4 +5,5 @@ export default {
|
||||
DELETED_ROOT_POST_ERROR: 'api.post.create_post.root_id.app_error',
|
||||
TOWN_SQUARE_READ_ONLY_ERROR: 'api.post.create_post.town_square_read_only',
|
||||
PLUGIN_DISMISSED_POST_ERROR: 'plugin.message_will_be_posted.dismiss_post',
|
||||
TEAM_MEMBERSHIP_DENIAL_ERROR_ID: 'api.team.add_members.user_denied',
|
||||
};
|
||||
|
||||
@@ -51,6 +51,8 @@ const WebsocketEvents = {
|
||||
THREAD_UPDATED: 'thread_updated',
|
||||
THREAD_FOLLOW_CHANGED: 'thread_follow_changed',
|
||||
THREAD_READ_CHANGED: 'thread_read_changed',
|
||||
DELETE_TEAM: 'delete_team',
|
||||
RESTORE_TEAM: 'restore_team',
|
||||
APPS_FRAMEWORK_REFRESH_BINDINGS: 'custom_com.mattermost.apps_refresh_bindings',
|
||||
CALLS_CHANNEL_ENABLED: `custom_${Calls.PluginId}_channel_enable_voice`,
|
||||
CALLS_CHANNEL_DISABLED: `custom_${Calls.PluginId}_channel_disable_voice`,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {FAVORITES_CATEGORY} from '@constants/categories';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {makeCategoryChannelId} from '@utils/categories';
|
||||
import {pluckUnique} from '@utils/helpers';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
import {observeChannelsByLastPostAt} from './channel';
|
||||
|
||||
@@ -38,16 +39,10 @@ export const queryCategoriesByTeamIds = (database: Database, teamIds: string[])
|
||||
|
||||
export async function prepareCategoriesAndCategoriesChannels(operator: ServerDataOperator, categories: CategoryWithChannels[], prune = false) {
|
||||
try {
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
const preparedCategories = prepareCategories(operator, categories);
|
||||
if (preparedCategories) {
|
||||
modelPromises.push(preparedCategories);
|
||||
}
|
||||
|
||||
const preparedCategoryChannels = prepareCategoryChannels(operator, categories);
|
||||
if (preparedCategoryChannels) {
|
||||
modelPromises.push(preparedCategoryChannels);
|
||||
}
|
||||
const modelPromises: Array<Promise<Model[]>> = [
|
||||
prepareCategories(operator, categories),
|
||||
prepareCategoryChannels(operator, categories),
|
||||
];
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
const flattenedModels = models.flat();
|
||||
@@ -71,7 +66,8 @@ export async function prepareCategoriesAndCategoriesChannels(operator: ServerDat
|
||||
}
|
||||
|
||||
return flattenedModels;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
logDebug('error while preparing categories and categories channels', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,31 +165,6 @@ export const getLastTeam = async (database: Database, ignoreIdForDefault?: strin
|
||||
return getDefaultTeamId(database, ignoreIdForDefault);
|
||||
};
|
||||
|
||||
export async function syncTeamTable(operator: ServerDataOperator, teams: Team[]) {
|
||||
try {
|
||||
const deletedTeams = teams.filter((t) => t.delete_at > 0).map((t) => t.id);
|
||||
const deletedSet = new Set(deletedTeams);
|
||||
const availableTeams = teams.filter((a) => !deletedSet.has(a.id));
|
||||
const models = [];
|
||||
|
||||
if (deletedTeams.length) {
|
||||
const notAvailable = await operator.database.get<TeamModel>(TEAM).query(Q.where('id', Q.oneOf(deletedTeams))).fetch();
|
||||
const deletions = await Promise.all(notAvailable.map((t) => prepareDeleteTeam(t)));
|
||||
for (const d of deletions) {
|
||||
models.push(...d);
|
||||
}
|
||||
}
|
||||
|
||||
models.push(...await operator.handleTeam({teams: availableTeams, prepareRecordsOnly: true}));
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
return {};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export const getDefaultTeamId = async (database: Database, ignoreId?: string) => {
|
||||
const user = await getCurrentUser(database);
|
||||
const config = await getConfig(database);
|
||||
|
||||
@@ -7,13 +7,13 @@ import FastImage from 'react-native-fast-image';
|
||||
import {RectButton, TouchableWithoutFeedback} from 'react-native-gesture-handler';
|
||||
import Animated from 'react-native-reanimated';
|
||||
|
||||
import {typography} from '@app/utils/typography';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {Preferences} from '@constants';
|
||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||
import {calculateDimensions} from '@utils/images';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
||||
@@ -25,7 +25,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
isCRTEnabled: observeIsCRTEnabled(database),
|
||||
teamsCount: queryMyTeams(database).observeCount(false),
|
||||
channelsCount: observeCurrentTeamId(database).pipe(
|
||||
switchMap((id) => (id ? queryAllMyChannelsForTeam(database, id).observeCount() : of$(0))),
|
||||
switchMap((id) => (id ? queryAllMyChannelsForTeam(database, id).observeCount(false) : of$(0))),
|
||||
),
|
||||
isLicensed,
|
||||
showToS: observeShowToS(database),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
|
||||
import TeamList from '@components/team_sidebar/add_team/team_list';
|
||||
import TeamList from '@components/team_list';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import BottomSheetContent from '@screens/bottom_sheet/content';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
|
||||
@@ -137,6 +137,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
case Screens.JOIN_TEAM:
|
||||
screen = withServerDatabase(require('@screens/join_team').default);
|
||||
break;
|
||||
case Screens.LATEX:
|
||||
screen = withServerDatabase(require('@screens/latex').default);
|
||||
break;
|
||||
|
||||
37
app/screens/join_team/index.ts
Normal file
37
app/screens/join_team/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {queryMyTeams} from '@queries/servers/team';
|
||||
import MyTeamModel from '@typings/database/models/servers/my_team';
|
||||
|
||||
import JoinTeam from './join_team';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const membershipsToIdSet = (mm: MyTeamModel[]) => {
|
||||
return new Set(mm.map((m) => m.id));
|
||||
};
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
// TODO https://mattermost.atlassian.net/browse/MM-43622
|
||||
// const canCreateTeams = observeCurrentUser(database).pipe(
|
||||
// switchMap((u) => (u ? of$(u.roles.split(' ')) : of$([]))),
|
||||
// switchMap((values) => queryRolesByNames(database, values).observeWithColumns(['permissions'])),
|
||||
// switchMap((r) => of$(hasPermission(r, Permissions.CREATE_TEAM, false))),
|
||||
// );
|
||||
|
||||
const joinedIds = queryMyTeams(database).observe().pipe(
|
||||
switchMap((mm) => of$(membershipsToIdSet(mm))),
|
||||
);
|
||||
|
||||
return {
|
||||
joinedIds,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(JoinTeam));
|
||||
155
app/screens/join_team/join_team.tsx
Normal file
155
app/screens/join_team/join_team.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import {addCurrentUserToTeam, fetchTeamsForComponent, handleTeamChange} from '@actions/remote/team';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import Empty from '@components/illustrations/no_team';
|
||||
import Loading from '@components/loading';
|
||||
import TeamList from '@components/team_list';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {dismissModal} from '@screens/navigation';
|
||||
import {logDebug} from '@utils/log';
|
||||
import {alertTeamAddError} from '@utils/navigation';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
joinedIds: Set<string>;
|
||||
componentId: string;
|
||||
closeButtonId: string;
|
||||
}
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 5,
|
||||
flex: 1,
|
||||
},
|
||||
empty: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
loading: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
marginTop: 16,
|
||||
...typography('Heading', 400, 'Regular'),
|
||||
},
|
||||
description: {
|
||||
color: theme.centerChannelColor,
|
||||
marginTop: 8,
|
||||
maxWidth: 334,
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function JoinTeam({
|
||||
joinedIds,
|
||||
componentId,
|
||||
closeButtonId,
|
||||
}: Props) {
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const intl = useIntl();
|
||||
const page = useRef(0);
|
||||
const hasMore = useRef(true);
|
||||
const mounted = useRef(true);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [joining, setJoining] = useState(false);
|
||||
const [otherTeams, setOtherTeams] = useState<Team[]>([]);
|
||||
|
||||
const loadTeams = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const resp = await fetchTeamsForComponent(serverUrl, page.current, joinedIds);
|
||||
page.current = resp.page;
|
||||
hasMore.current = resp.hasMore;
|
||||
if (resp.teams.length && mounted.current) {
|
||||
setOtherTeams((cur) => [...cur, ...resp.teams]);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [joinedIds, serverUrl]);
|
||||
|
||||
const onEndReached = useCallback(() => {
|
||||
if (hasMore.current && !loading) {
|
||||
loadTeams();
|
||||
}
|
||||
}, [loadTeams, loading]);
|
||||
|
||||
const onPress = useCallback(async (teamId: string) => {
|
||||
setJoining(true);
|
||||
const {error} = await addCurrentUserToTeam(serverUrl, teamId);
|
||||
if (error) {
|
||||
alertTeamAddError(error, intl);
|
||||
logDebug('error joining a team:', error);
|
||||
setJoining(false);
|
||||
} else {
|
||||
handleTeamChange(serverUrl, teamId);
|
||||
dismissModal({componentId});
|
||||
}
|
||||
}, [serverUrl, componentId, intl]);
|
||||
|
||||
useEffect(() => {
|
||||
loadTeams();
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onClosePressed = useCallback(() => {
|
||||
return dismissModal({componentId});
|
||||
}, [componentId]);
|
||||
|
||||
useNavButtonPressed(closeButtonId, componentId, onClosePressed, []);
|
||||
|
||||
const hasOtherTeams = Boolean(otherTeams.length);
|
||||
|
||||
let body;
|
||||
if ((loading && !hasOtherTeams) || joining) {
|
||||
body = (<Loading containerStyle={styles.loading}/>);
|
||||
} else if (hasOtherTeams) {
|
||||
body = (
|
||||
<TeamList
|
||||
teams={otherTeams}
|
||||
onPress={onPress}
|
||||
testID='team_sidebar.add_team_slide_up.team_list'
|
||||
onEndReached={onEndReached}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
body = (
|
||||
<View style={styles.empty}>
|
||||
<Empty theme={theme}/>
|
||||
<FormattedText
|
||||
id='team_list.no_other_teams.title'
|
||||
defaultMessage='No additional teams to join'
|
||||
style={styles.title}
|
||||
testID='team_sidebar.add_team_slide_up.no_other_teams.title'
|
||||
/>
|
||||
<FormattedText
|
||||
id='team_list.no_other_teams.description'
|
||||
defaultMessage='To join another team, ask a Team Admin for an invitation, or create your own team.'
|
||||
style={styles.description}
|
||||
testID='team_sidebar.add_team_slide_up.no_other_teams.description'
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{body}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import {useIsTablet} from '@hooks/device';
|
||||
import {t} from '@i18n';
|
||||
import {goToScreen, loginAnimationOptions, resetToHome, resetToTeams} from '@screens/navigation';
|
||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||
import {isServerError} from '@utils/errors';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
@@ -139,9 +140,9 @@ const LoginForm = ({config, extra, keyboardAwareRef, numberSSOs, serverDisplayNa
|
||||
|
||||
const checkLoginResponse = (data: LoginActionResponse) => {
|
||||
let errorId = '';
|
||||
const clientError = data.error as ClientErrorProps;
|
||||
if (clientError && clientError.server_error_id) {
|
||||
errorId = clientError.server_error_id;
|
||||
const loginError = data.error;
|
||||
if (isServerError(loginError) && loginError.server_error_id) {
|
||||
errorId = loginError.server_error_id;
|
||||
}
|
||||
|
||||
if (data.failed && MFA_EXPECTED_ERRORS.includes(errorId)) {
|
||||
@@ -150,9 +151,9 @@ const LoginForm = ({config, extra, keyboardAwareRef, numberSSOs, serverDisplayNa
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data?.error && data.failed) {
|
||||
if (loginError && data.failed) {
|
||||
setIsLoading(false);
|
||||
setError(getLoginErrorMessage(data.error));
|
||||
setError(getLoginErrorMessage(loginError));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observePost} from '@queries/servers/post';
|
||||
import {observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeCurrentTeamId} from '@queries/servers/system';
|
||||
import {queryMyTeamsByIds, queryTeamByName} from '@queries/servers/team';
|
||||
import {observeIsCRTEnabled} from '@queries/servers/thread';
|
||||
|
||||
@@ -42,7 +42,6 @@ const enhance = withObservables([], ({database, postId, teamName}: OwnProps) =>
|
||||
switchMap((ms) => of$(Boolean(ms?.[0]))),
|
||||
),
|
||||
currentTeamId: observeCurrentTeamId(database),
|
||||
currentUserId: observeCurrentUserId(database),
|
||||
isCRTEnabled: observeIsCRTEnabled(database),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-cont
|
||||
|
||||
import {fetchChannelById, joinChannel, switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchPostById, fetchPostsAround, fetchPostThread} from '@actions/remote/post';
|
||||
import {addUserToTeam, fetchTeamByName, removeUserFromTeam} from '@actions/remote/team';
|
||||
import {addCurrentUserToTeam, fetchTeamByName, removeCurrentUserFromTeam} from '@actions/remote/team';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import Loading from '@components/loading';
|
||||
@@ -37,7 +37,6 @@ type Props = {
|
||||
rootId?: string;
|
||||
teamName?: string;
|
||||
isTeamMember?: boolean;
|
||||
currentUserId: string;
|
||||
currentTeamId: string;
|
||||
isCRTEnabled: boolean;
|
||||
postId: PostModel['id'];
|
||||
@@ -130,7 +129,6 @@ function Permalink({
|
||||
postId,
|
||||
teamName,
|
||||
isTeamMember,
|
||||
currentUserId,
|
||||
currentTeamId,
|
||||
}: Props) {
|
||||
const [posts, setPosts] = useState<PostModel[]>([]);
|
||||
@@ -187,7 +185,7 @@ function Permalink({
|
||||
joinedTeam = fetchData.team;
|
||||
|
||||
if (joinedTeam) {
|
||||
const addData = await addUserToTeam(serverUrl, joinedTeam.id, currentUserId);
|
||||
const addData = await addCurrentUserToTeam(serverUrl, joinedTeam.id);
|
||||
if (addData.error) {
|
||||
joinedTeam = undefined;
|
||||
}
|
||||
@@ -197,7 +195,7 @@ function Permalink({
|
||||
const {post} = await fetchPostById(serverUrl, postId, true);
|
||||
if (!post) {
|
||||
if (joinedTeam) {
|
||||
removeUserFromTeam(serverUrl, joinedTeam.id, currentUserId);
|
||||
removeCurrentUserFromTeam(serverUrl, joinedTeam.id);
|
||||
}
|
||||
setError({notExist: true});
|
||||
setLoading(false);
|
||||
@@ -210,7 +208,7 @@ function Permalink({
|
||||
|
||||
// Wrong team passed or DM/GM
|
||||
if (joinedTeam && localChannel?.teamId !== '' && localChannel?.teamId !== joinedTeam.id) {
|
||||
removeUserFromTeam(serverUrl, joinedTeam.id, currentUserId);
|
||||
removeCurrentUserFromTeam(serverUrl, joinedTeam.id);
|
||||
joinedTeam = undefined;
|
||||
}
|
||||
|
||||
@@ -233,7 +231,7 @@ function Permalink({
|
||||
const {channel: fetchedChannel} = await fetchChannelById(serverUrl, post.channel_id);
|
||||
if (!fetchedChannel) {
|
||||
if (joinedTeam) {
|
||||
removeUserFromTeam(serverUrl, joinedTeam.id, currentUserId);
|
||||
removeCurrentUserFromTeam(serverUrl, joinedTeam.id);
|
||||
}
|
||||
setError({notExist: true});
|
||||
setLoading(false);
|
||||
@@ -242,7 +240,7 @@ function Permalink({
|
||||
|
||||
// Wrong team passed or DM/GM
|
||||
if (joinedTeam && fetchedChannel.team_id !== '' && fetchedChannel.team_id !== joinedTeam.id) {
|
||||
removeUserFromTeam(serverUrl, joinedTeam.id, currentUserId);
|
||||
removeCurrentUserFromTeam(serverUrl, joinedTeam.id);
|
||||
joinedTeam = undefined;
|
||||
}
|
||||
|
||||
@@ -261,7 +259,7 @@ function Permalink({
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (error?.joinedTeam && error.teamId) {
|
||||
removeUserFromTeam(serverUrl, error.teamId, currentUserId);
|
||||
removeCurrentUserFromTeam(serverUrl, error.teamId);
|
||||
}
|
||||
dismissModal({componentId: Screens.PERMALINK});
|
||||
closePermalink();
|
||||
@@ -288,7 +286,7 @@ function Permalink({
|
||||
}
|
||||
setChannelId(error.channelId);
|
||||
}
|
||||
}), [error, serverUrl, currentUserId]);
|
||||
}), [error, serverUrl]);
|
||||
|
||||
let content;
|
||||
if (loading) {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {queryMyTeams} from '@queries/servers/team';
|
||||
|
||||
@@ -18,10 +20,12 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
// switchMap((r) => of$(hasPermission(r, Permissions.CREATE_TEAM, false))),
|
||||
// );
|
||||
|
||||
const nTeams = queryMyTeams(database).observeCount();
|
||||
|
||||
const myTeams = queryMyTeams(database).observe();
|
||||
const nTeams = myTeams.pipe(switchMap((mm) => of$(mm.length)));
|
||||
const firstTeamId = myTeams.pipe(switchMap((mm) => of$(mm[0]?.id)));
|
||||
return {
|
||||
nTeams,
|
||||
firstTeamId,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {View} from 'react-native';
|
||||
import Animated, {useAnimatedStyle} from 'react-native-reanimated';
|
||||
import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {fetchAllTeams} from '@actions/remote/team';
|
||||
import {addCurrentUserToTeam, fetchTeamsForComponent, handleTeamChange} from '@actions/remote/team';
|
||||
import Loading from '@components/loading';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {logDebug} from '@utils/log';
|
||||
import {alertTeamAddError} from '@utils/navigation';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import {resetToHome} from '../navigation';
|
||||
@@ -22,10 +26,16 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
flex: 1,
|
||||
backgroundColor: theme.sidebarBg,
|
||||
},
|
||||
loading: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
nTeams: number;
|
||||
firstTeamId?: string;
|
||||
}
|
||||
|
||||
const safeAreaEdges = ['left' as const, 'right' as const];
|
||||
@@ -33,19 +43,56 @@ const safeAreaStyle = {flex: 1};
|
||||
|
||||
const SelectTeam = ({
|
||||
nTeams,
|
||||
firstTeamId,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const serverUrl = useServerUrl();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const intl = useIntl();
|
||||
const insets = useSafeAreaInsets();
|
||||
const resettingToHome = useRef(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [joining, setJoining] = useState(false);
|
||||
const top = useAnimatedStyle(() => {
|
||||
return {height: insets.top, backgroundColor: theme.sidebarBg};
|
||||
});
|
||||
|
||||
const page = useRef(0);
|
||||
const hasMore = useRef(true);
|
||||
|
||||
const mounted = useRef(false);
|
||||
|
||||
const [otherTeams, setOtherTeams] = useState<Team[]>();
|
||||
const [otherTeams, setOtherTeams] = useState<Team[]>([]);
|
||||
|
||||
const loadTeams = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const resp = await fetchTeamsForComponent(serverUrl, page.current);
|
||||
page.current = resp.page;
|
||||
hasMore.current = resp.hasMore;
|
||||
if (resp.teams.length && mounted.current) {
|
||||
setOtherTeams((cur) => [...cur, ...resp.teams]);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [serverUrl]);
|
||||
|
||||
const onEndReached = useCallback(() => {
|
||||
if (hasMore.current && !loading) {
|
||||
loadTeams();
|
||||
}
|
||||
}, [loadTeams, loading]);
|
||||
|
||||
const onTeamPressed = useCallback(async (teamId: string) => {
|
||||
setJoining(true);
|
||||
const {error} = await addCurrentUserToTeam(serverUrl, teamId);
|
||||
if (error) {
|
||||
alertTeamAddError(error, intl);
|
||||
logDebug('error joining a team:', error);
|
||||
|
||||
setJoining(false);
|
||||
}
|
||||
|
||||
// Back to home handled in an effect
|
||||
}, [serverUrl, intl]);
|
||||
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
@@ -55,32 +102,32 @@ const SelectTeam = ({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (nTeams > 0) {
|
||||
resetToHome();
|
||||
if (resettingToHome.current) {
|
||||
return;
|
||||
}
|
||||
}, [nTeams > 0]);
|
||||
|
||||
if ((nTeams > 0) && firstTeamId) {
|
||||
resettingToHome.current = true;
|
||||
handleTeamChange(serverUrl, firstTeamId).then(() => {
|
||||
resetToHome();
|
||||
});
|
||||
}
|
||||
}, [(nTeams > 0) && firstTeamId]);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
fetchAllTeams(serverUrl, false).then((r) => {
|
||||
if (mounted.current) {
|
||||
setOtherTeams(r.teams || []);
|
||||
}
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
}).finally(() => {
|
||||
if (mounted.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
loadTeams();
|
||||
}, []);
|
||||
|
||||
let body;
|
||||
if (loading) {
|
||||
body = null;
|
||||
} else if (otherTeams?.length) {
|
||||
if (joining || (loading && !otherTeams.length)) {
|
||||
body = <Loading containerStyle={styles.loading}/>;
|
||||
} else if (otherTeams.length) {
|
||||
body = (
|
||||
<TeamList
|
||||
teams={otherTeams}
|
||||
onEndReached={onEndReached}
|
||||
onPress={onTeamPressed}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -4,12 +4,9 @@ import React, {useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import {handleTeamChange} from '@actions/remote/team';
|
||||
import TeamFlatList from '@components/team_sidebar/add_team/team_list';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import TeamFlatList from '@components/team_list';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {resetToHome} from '@screens/navigation';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
@@ -39,21 +36,21 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
|
||||
type Props = {
|
||||
teams: Team[];
|
||||
onEndReached: () => void;
|
||||
onPress: (id: string) => void;
|
||||
loading: boolean;
|
||||
};
|
||||
function TeamList({
|
||||
teams,
|
||||
onEndReached,
|
||||
onPress,
|
||||
loading,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const isTablet = useIsTablet();
|
||||
|
||||
const onTeamAdded = async (id: string) => {
|
||||
await handleTeamChange(serverUrl, id);
|
||||
resetToHome();
|
||||
};
|
||||
|
||||
const containerStyle = useMemo(() => {
|
||||
return isTablet ? [styles.container, {maxWidth: 600, alignItems: 'center' as const}] : styles.container;
|
||||
}, [isTablet, styles]);
|
||||
@@ -75,7 +72,9 @@ function TeamList({
|
||||
textColor={theme.sidebarText}
|
||||
iconBackgroundColor={changeOpacity(theme.sidebarText, 0.16)}
|
||||
iconTextColor={theme.sidebarText}
|
||||
onPress={onTeamAdded}
|
||||
onPress={onPress}
|
||||
onEndReached={onEndReached}
|
||||
loading={loading}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
class EphemeralStore {
|
||||
theme: Theme | undefined;
|
||||
creatingChannel = false;
|
||||
creatingDMorGMTeammates: string[] = [];
|
||||
|
||||
private pushProxyVerification: {[x: string]: string | undefined} = {};
|
||||
private pushProxyVerification: {[serverUrl: string]: string | undefined} = {};
|
||||
private canJoinOtherTeams: {[serverUrl: string]: BehaviorSubject<boolean>} = {};
|
||||
|
||||
// As of today, the server sends a duplicated event to add the user to the team.
|
||||
// If we do not handle this, this ends up showing some errors in the database, apart
|
||||
@@ -115,6 +118,22 @@ class EphemeralStore {
|
||||
removeSwitchingToChannel = (channelId: string) => {
|
||||
this.switchingToChannel.delete(channelId);
|
||||
};
|
||||
|
||||
private getCanJoinOtherTeamsSubject = (serverUrl: string) => {
|
||||
if (!this.canJoinOtherTeams[serverUrl]) {
|
||||
this.canJoinOtherTeams[serverUrl] = new BehaviorSubject(false);
|
||||
}
|
||||
|
||||
return this.canJoinOtherTeams[serverUrl];
|
||||
};
|
||||
|
||||
observeCanJoinOtherTeams = (serverUrl: string) => {
|
||||
return this.getCanJoinOtherTeamsSubject(serverUrl).asObservable();
|
||||
};
|
||||
|
||||
setCanJoinOtherTeams = (serverUrl: string, value: boolean) => {
|
||||
this.getCanJoinOtherTeamsSubject(serverUrl).next(value);
|
||||
};
|
||||
}
|
||||
|
||||
export default new EphemeralStore();
|
||||
|
||||
11
app/utils/errors.ts
Normal file
11
app/utils/errors.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export function isServerError(obj: unknown): obj is {server_error_id: string; message?: string} {
|
||||
return (
|
||||
typeof obj === 'object' &&
|
||||
obj !== null &&
|
||||
'server_error_id' in obj &&
|
||||
typeof obj.server_error_id === 'string'
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import {IntlShape} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
import {Navigation, Options} from 'react-native-navigation';
|
||||
|
||||
import {Screens} from '@constants';
|
||||
import {Screens, ServerErrors} from '@constants';
|
||||
import {isServerError} from '@utils/errors';
|
||||
|
||||
export const appearanceControlledScreens = new Set([
|
||||
Screens.ONBOARDING,
|
||||
@@ -72,3 +73,23 @@ export function alertChannelArchived(displayName: string, intl: IntlShape) {
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
export function alertTeamAddError(error: unknown, intl: IntlShape) {
|
||||
let errMsg = intl.formatMessage({id: 'join_team.error.message', defaultMessage: 'There has been an error joining the team.'});
|
||||
|
||||
if (isServerError(error)) {
|
||||
if (error.server_error_id === ServerErrors.TEAM_MEMBERSHIP_DENIAL_ERROR_ID) {
|
||||
errMsg = intl.formatMessage({
|
||||
id: 'join_team.error.group_error',
|
||||
defaultMessage: 'You need to be a member of a linked group to join this team.',
|
||||
});
|
||||
} else if (error.message) {
|
||||
errMsg = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
intl.formatMessage({id: 'join_team.error.title', defaultMessage: 'Error joining a team'}),
|
||||
errMsg,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import {ScaledSize} from 'react-native';
|
||||
import {EdgeInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {ITEM_HEIGHT} from '@components/team_sidebar/add_team/team_list_item/team_list_item';
|
||||
import {ITEM_HEIGHT} from '@components/team_list/team_list_item/team_list_item';
|
||||
import {PADDING_TOP_MOBILE} from '@screens/bottom_sheet';
|
||||
import {TITLE_HEIGHT, TITLE_SEPARATOR_MARGIN} from '@screens/bottom_sheet/content';
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
|
||||
@@ -326,6 +326,9 @@
|
||||
"intro.welcome.public": "Add some more team members to the channel or start a conversation below.",
|
||||
"invite_people_to_team.message": "Here’s a link to collaborate and communicate with us on Mattermost.",
|
||||
"invite_people_to_team.title": "Join the {team} team",
|
||||
"join_team.error.group_error": "You need to be a member of a linked group to join this team.",
|
||||
"join_team.error.message": "There has been an error joining the team",
|
||||
"join_team.error.title": "Error joining a team",
|
||||
"last_users_message.added_to_channel.type": "were **added to the channel** by {actor}.",
|
||||
"last_users_message.added_to_team.type": "were **added to the team** by {actor}.",
|
||||
"last_users_message.first": "{firstUser} and ",
|
||||
|
||||
Reference in New Issue
Block a user