forked from Ivasoft/mattermost-mobile
[Gekidou] [MM-39936] Add Select Team Screen (#6180)
* Add Select Team Screen * Fixes for iPhone and iPad * Fix tests * Address feedback * Fix tests * Theme illustration * Address feedback and fixes * Remove database warnings by avoiding recalculations on repeated events. * Address feedback * Remove unneeded catch Co-authored-by: Daniel Espino <danielespino@MacBook-Pro-de-Daniel.local>
This commit is contained in:
committed by
GitHub
parent
2376dc934c
commit
02b4295464
@@ -46,7 +46,7 @@ export async function appEntry(serverUrl: string, since = 0) {
|
||||
}
|
||||
|
||||
const {initialTeamId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds} = fetchedData as AppEntryData;
|
||||
const rolesData = await fetchRoles(serverUrl, teamData?.memberships, chData?.memberships, meData?.user, true);
|
||||
const rolesData = await fetchRoles(serverUrl, teamData?.memberships, chData?.memberships, meData?.user, true, true);
|
||||
|
||||
if (initialTeamId === currentTeamId) {
|
||||
if (tabletDevice) {
|
||||
|
||||
@@ -12,7 +12,7 @@ export type RolesRequest = {
|
||||
roles?: Role[];
|
||||
}
|
||||
|
||||
export const fetchRolesIfNeeded = async (serverUrl: string, updatedRoles: string[], fetchOnly = false): Promise<RolesRequest> => {
|
||||
export const fetchRolesIfNeeded = async (serverUrl: string, updatedRoles: string[], fetchOnly = false, force = false): Promise<RolesRequest> => {
|
||||
if (!updatedRoles.length) {
|
||||
return {roles: []};
|
||||
}
|
||||
@@ -26,15 +26,20 @@ export const fetchRolesIfNeeded = async (serverUrl: string, updatedRoles: string
|
||||
|
||||
const database = DatabaseManager.serverDatabases[serverUrl].database;
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl].operator;
|
||||
const existingRoles = await queryRoles(database).fetch();
|
||||
let newRoles;
|
||||
if (force) {
|
||||
newRoles = updatedRoles;
|
||||
} else {
|
||||
const existingRoles = await queryRoles(database).fetch();
|
||||
|
||||
const roleNames = new Set(existingRoles.map((role) => {
|
||||
return role.name;
|
||||
}));
|
||||
const roleNames = new Set(existingRoles.map((role) => {
|
||||
return role.name;
|
||||
}));
|
||||
|
||||
const newRoles = updatedRoles.filter((newRole) => {
|
||||
return !roleNames.has(newRole);
|
||||
});
|
||||
newRoles = updatedRoles.filter((newRole) => {
|
||||
return !roleNames.has(newRole);
|
||||
});
|
||||
}
|
||||
|
||||
if (!newRoles.length) {
|
||||
return {roles: []};
|
||||
@@ -56,7 +61,7 @@ export const fetchRolesIfNeeded = async (serverUrl: string, updatedRoles: string
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchRoles = async (serverUrl: string, teamMembership?: TeamMembership[], channelMembership?: ChannelMembership[], user?: UserProfile, fetchOnly = false) => {
|
||||
export const fetchRoles = async (serverUrl: string, teamMembership?: TeamMembership[], channelMembership?: ChannelMembership[], user?: UserProfile, fetchOnly = false, force = false) => {
|
||||
const rolesToFetch = new Set<string>(user?.roles.split(' ') || []);
|
||||
|
||||
if (teamMembership?.length) {
|
||||
@@ -78,7 +83,7 @@ export const fetchRoles = async (serverUrl: string, teamMembership?: TeamMembers
|
||||
|
||||
rolesToFetch.delete('');
|
||||
if (rolesToFetch.size > 0) {
|
||||
return fetchRolesIfNeeded(serverUrl, Array.from(rolesToFetch), fetchOnly);
|
||||
return fetchRolesIfNeeded(serverUrl, Array.from(rolesToFetch), fetchOnly, force);
|
||||
}
|
||||
|
||||
return {roles: []};
|
||||
|
||||
@@ -13,6 +13,7 @@ import {prepareCategories, prepareCategoryChannels} from '@queries/servers/categ
|
||||
import {prepareMyChannelsForTeam, getDefaultChannelForTeam} from '@queries/servers/channel';
|
||||
import {prepareCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected} from '@queries/servers/system';
|
||||
import {addTeamToTeamHistory, prepareDeleteTeam, prepareMyTeams, getNthLastChannelFromTeam, queryTeamsById, syncTeamTable} from '@queries/servers/team';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
|
||||
import {fetchMyChannelsForTeam, switchToChannelById} from './channel';
|
||||
@@ -37,6 +38,7 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s
|
||||
}
|
||||
|
||||
try {
|
||||
EphemeralStore.startAddingToTeam(teamId);
|
||||
const member = await client.addToTeam(teamId, userId);
|
||||
|
||||
if (!fetchOnly) {
|
||||
@@ -67,9 +69,10 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EphemeralStore.finishAddingToTeam(teamId);
|
||||
return {member};
|
||||
} catch (error) {
|
||||
EphemeralStore.finishAddingToTeam(teamId);
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
|
||||
@@ -251,6 +251,9 @@ export async function handleUserAddedToChannelEvent(serverUrl: string, msg: any)
|
||||
|
||||
try {
|
||||
if (userId === currentUser?.id) {
|
||||
if (EphemeralStore.isAddingToTeam(teamId)) {
|
||||
return;
|
||||
}
|
||||
const {channels, memberships} = await fetchMyChannel(serverUrl, teamId, channelId, true);
|
||||
if (channels && memberships) {
|
||||
const prepare = await prepareMyChannelsForTeam(operator, teamId, channels, memberships);
|
||||
|
||||
@@ -5,16 +5,19 @@ import {Model} from '@nozbe/watermelondb';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {removeUserFromTeam} from '@actions/local/team';
|
||||
import {fetchRolesIfNeeded} from '@actions/remote/role';
|
||||
import {fetchMyChannelsForTeam} from '@actions/remote/channel';
|
||||
import {fetchRoles} from '@actions/remote/role';
|
||||
import {fetchAllTeams, handleTeamChange, fetchMyTeam} from '@actions/remote/team';
|
||||
import {updateUsersNoLongerVisible} from '@actions/remote/user';
|
||||
import Events from '@constants/events';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getActiveServerUrl} from '@queries/app/servers';
|
||||
import {getCurrentTeamId} from '@queries/servers/system';
|
||||
import {getLastTeam, prepareMyTeams} from '@queries/servers/team';
|
||||
import {prepareCategories, prepareCategoryChannels} from '@queries/servers/categories';
|
||||
import {prepareMyChannelsForTeam} from '@queries/servers/channel';
|
||||
import {getCurrentTeam, getLastTeam, prepareMyTeams} from '@queries/servers/team';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {dismissAllModals, popToRoot} from '@screens/navigation';
|
||||
import {dismissAllModals, popToRoot, resetToTeams} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
|
||||
export async function handleLeaveTeamEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl];
|
||||
@@ -22,7 +25,7 @@ export async function handleLeaveTeamEvent(serverUrl: string, msg: WebSocketMess
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTeamId = await getCurrentTeamId(database.database);
|
||||
const currentTeam = await getCurrentTeam(database.database);
|
||||
const user = await getCurrentUser(database.database);
|
||||
if (!user) {
|
||||
return;
|
||||
@@ -37,7 +40,7 @@ export async function handleLeaveTeamEvent(serverUrl: string, msg: WebSocketMess
|
||||
updateUsersNoLongerVisible(serverUrl);
|
||||
}
|
||||
|
||||
if (currentTeamId === teamId) {
|
||||
if (currentTeam?.id === teamId) {
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
let currentServer = '';
|
||||
if (appDatabase) {
|
||||
@@ -45,7 +48,7 @@ export async function handleLeaveTeamEvent(serverUrl: string, msg: WebSocketMess
|
||||
}
|
||||
|
||||
if (currentServer === serverUrl) {
|
||||
DeviceEventEmitter.emit(Events.LEAVE_TEAM);
|
||||
DeviceEventEmitter.emit(Events.LEAVE_TEAM, currentTeam?.displayName);
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
}
|
||||
@@ -53,7 +56,9 @@ export async function handleLeaveTeamEvent(serverUrl: string, msg: WebSocketMess
|
||||
const teamToJumpTo = await getLastTeam(database.database);
|
||||
if (teamToJumpTo) {
|
||||
handleTeamChange(serverUrl, teamToJumpTo);
|
||||
} // TODO else jump to "join a team" screen
|
||||
} else if (currentServer === serverUrl) {
|
||||
resetToTeams();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,57 +75,43 @@ export async function handleUpdateTeamEvent(serverUrl: string, msg: WebSocketMes
|
||||
teams: [team],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
} catch {
|
||||
} catch (err) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// of the extra computation time. We use this to track the events that are being handled
|
||||
// and make sure we only handle one.
|
||||
const addingTeam: {[id: string]: boolean} = {};
|
||||
|
||||
export async function handleUserAddedToTeamEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl];
|
||||
if (!database) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return;
|
||||
}
|
||||
const {team_id: teamId} = msg.data;
|
||||
|
||||
// Ignore duplicated team join events sent by the server
|
||||
if (addingTeam[teamId]) {
|
||||
if (EphemeralStore.isAddingToTeam(teamId)) {
|
||||
return;
|
||||
}
|
||||
addingTeam[teamId] = true;
|
||||
EphemeralStore.startAddingToTeam(teamId);
|
||||
|
||||
const {teams, memberships: teamMemberships} = await fetchMyTeam(serverUrl, teamId, true);
|
||||
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
if (teams?.length && teamMemberships?.length) {
|
||||
const myMember = teamMemberships[0];
|
||||
if (myMember.roles) {
|
||||
const rolesToLoad = new Set<string>();
|
||||
for (const role of myMember.roles.split(' ')) {
|
||||
rolesToLoad.add(role);
|
||||
}
|
||||
const serverRoles = await fetchRolesIfNeeded(serverUrl, Array.from(rolesToLoad), true);
|
||||
if (serverRoles.roles?.length) {
|
||||
const preparedRoleModels = database.operator.handleRole({
|
||||
roles: serverRoles.roles,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
modelPromises.push(preparedRoleModels);
|
||||
}
|
||||
}
|
||||
const {channels, memberships, categories} = await fetchMyChannelsForTeam(serverUrl, teamId, false, 0, true);
|
||||
modelPromises.push(prepareCategories(operator, categories));
|
||||
modelPromises.push(prepareCategoryChannels(operator, categories));
|
||||
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(database.operator, teams, teamMemberships));
|
||||
modelPromises.push(...prepareMyTeams(operator, teams, teamMemberships));
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await database.operator.batchRecords(models.flat());
|
||||
await operator.batchRecords(models.flat());
|
||||
|
||||
delete addingTeam[teamId];
|
||||
EphemeralStore.finishAddingToTeam(teamId);
|
||||
}
|
||||
|
||||
270
app/components/illustrations/no_team.tsx
Normal file
270
app/components/illustrations/no_team.tsx
Normal file
File diff suppressed because one or more lines are too long
@@ -73,78 +73,55 @@ exports[`Loading Error should match snapshot 1`] = `
|
||||
Error description
|
||||
</Text>
|
||||
<View
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderRadius": 4,
|
||||
"flex": 0,
|
||||
"height": 48,
|
||||
"justifyContent": "center",
|
||||
"marginTop": 24,
|
||||
"opacity": 1,
|
||||
"paddingHorizontal": 24,
|
||||
"paddingVertical": 14,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"marginTop": 24,
|
||||
"alignItems": "center",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontWeight": "600",
|
||||
"justifyContent": "center",
|
||||
"padding": 1,
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
Object {
|
||||
"fontSize": 16,
|
||||
"lineHeight": 18,
|
||||
"marginTop": 1,
|
||||
},
|
||||
Object {
|
||||
"color": "#1c58d9",
|
||||
},
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"borderRadius": 4,
|
||||
"flex": 0,
|
||||
"justifyContent": "center",
|
||||
},
|
||||
Object {
|
||||
"height": 48,
|
||||
"paddingHorizontal": 24,
|
||||
"paddingVertical": 14,
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
},
|
||||
],
|
||||
]
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontWeight": "600",
|
||||
"justifyContent": "center",
|
||||
"padding": 1,
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
Object {
|
||||
"fontSize": 16,
|
||||
"lineHeight": 16,
|
||||
"marginTop": 1,
|
||||
},
|
||||
Object {
|
||||
"color": "#1c58d9",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
Retry
|
||||
</Text>
|
||||
</View>
|
||||
Retry
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
@@ -84,6 +84,7 @@ const LoadingError = ({loading, message, onRetry, title}: Props) => {
|
||||
<TouchableWithFeedback
|
||||
style={buttonStyle}
|
||||
onPress={onRetry}
|
||||
type={'opacity'}
|
||||
>
|
||||
<Text style={buttonTextStyle(theme, 'lg', 'primary', 'inverted')}>{'Retry'}</Text>
|
||||
</TouchableWithFeedback>
|
||||
|
||||
@@ -21,14 +21,18 @@ export default function AddTeamSlideUp({otherTeams, canCreateTeams, showTitle =
|
||||
const intl = useIntl();
|
||||
|
||||
const onPressCreate = useCallback(() => {
|
||||
//TODO Create team screen
|
||||
//TODO Create team screen https://mattermost.atlassian.net/browse/MM-43622
|
||||
dismissBottomSheet();
|
||||
}, []);
|
||||
|
||||
const onTeamAdded = useCallback(() => {
|
||||
dismissBottomSheet();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BottomSheetContent
|
||||
buttonIcon='plus'
|
||||
buttonText={intl.formatMessage({id: 'mobile.add_team.create_team', defaultMessage: 'Create a New Team'})}
|
||||
buttonText={intl.formatMessage({id: 'mobile.add_team.create_team', defaultMessage: 'Create a new team'})}
|
||||
onPress={onPressCreate}
|
||||
showButton={canCreateTeams}
|
||||
showTitle={showTitle}
|
||||
@@ -37,6 +41,7 @@ export default function AddTeamSlideUp({otherTeams, canCreateTeams, showTitle =
|
||||
>
|
||||
<TeamList
|
||||
teams={otherTeams}
|
||||
onTeamAdded={onTeamAdded}
|
||||
testID='team_sidebar.add_team_slide_up.team_list'
|
||||
/>
|
||||
</BottomSheetContent>
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 82 KiB |
@@ -1,11 +1,12 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import React, {useCallback} from 'react';
|
||||
import {ListRenderItemInfo, 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 FormattedText from '@components/formatted_text';
|
||||
import Empty from '@components/illustrations/no_team';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
@@ -13,10 +14,12 @@ import TeamListItem from './team_list_item';
|
||||
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
const Empty = require('./no_teams.svg').default;
|
||||
|
||||
type Props = {
|
||||
teams: TeamModel[];
|
||||
teams: Array<Team|TeamModel>;
|
||||
textColor?: string;
|
||||
iconTextColor?: string;
|
||||
iconBackgroundColor?: string;
|
||||
onTeamAdded: (id: string) => void;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
@@ -49,20 +52,24 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const renderTeam = ({item: t}: ListRenderItemInfo<TeamModel>) => {
|
||||
return (
|
||||
<TeamListItem
|
||||
team={t}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const keyExtractor = (item: TeamModel) => item.id;
|
||||
|
||||
export default function TeamList({teams, testID}: Props) {
|
||||
export default function TeamList({teams, textColor, iconTextColor, iconBackgroundColor, onTeamAdded, testID}: Props) {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const renderTeam = useCallback(({item: t}: ListRenderItemInfo<Team|TeamModel>) => {
|
||||
return (
|
||||
<TeamListItem
|
||||
team={t}
|
||||
textColor={textColor}
|
||||
iconBackgroundColor={iconBackgroundColor}
|
||||
iconTextColor={iconTextColor}
|
||||
onTeamAdded={onTeamAdded}
|
||||
/>
|
||||
);
|
||||
}, [textColor, iconTextColor, iconBackgroundColor, onTeamAdded]);
|
||||
|
||||
if (teams.length) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
@@ -79,7 +86,7 @@ export default function TeamList({teams, testID}: Props) {
|
||||
|
||||
return (
|
||||
<View style={styles.empty}>
|
||||
<Empty/>
|
||||
<Empty theme={theme}/>
|
||||
<FormattedText
|
||||
id='team_list.no_other_teams.title'
|
||||
defaultMessage='No additional teams to join'
|
||||
|
||||
@@ -9,15 +9,18 @@ import TeamIcon from '@components/team_sidebar/team_list/team_item/team_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
type Props = {
|
||||
team: TeamModel;
|
||||
team: TeamModel | Team;
|
||||
currentUserId: string;
|
||||
textColor?: string;
|
||||
iconTextColor?: string;
|
||||
iconBackgroundColor?: string;
|
||||
onTeamAdded: (teamId: string) => void;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
@@ -46,15 +49,17 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
};
|
||||
});
|
||||
|
||||
export default function TeamListItem({team, currentUserId}: Props) {
|
||||
export default function TeamListItem({team, currentUserId, textColor, iconTextColor, iconBackgroundColor, onTeamAdded}: Props) {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const serverUrl = useServerUrl();
|
||||
const onPress = useCallback(async () => {
|
||||
await addUserToTeam(serverUrl, team.id, currentUserId);
|
||||
dismissBottomSheet();
|
||||
}, []);
|
||||
onTeamAdded(team.id);
|
||||
}, [onTeamAdded]);
|
||||
|
||||
const displayName = 'displayName' in team ? team.displayName : team.display_name;
|
||||
const lastTeamIconUpdateAt = 'lastTeamIconUpdatedAt' in team ? team.lastTeamIconUpdatedAt : team.last_team_icon_update;
|
||||
const teamListItemTestId = `team_sidebar.team_list.team_list_item.${team.id}`;
|
||||
|
||||
return (
|
||||
@@ -67,16 +72,20 @@ export default function TeamListItem({team, currentUserId}: Props) {
|
||||
<View style={styles.icon_container}>
|
||||
<TeamIcon
|
||||
id={team.id}
|
||||
displayName={team.displayName}
|
||||
lastIconUpdate={team.lastTeamIconUpdatedAt}
|
||||
displayName={displayName}
|
||||
lastIconUpdate={lastTeamIconUpdateAt}
|
||||
selected={false}
|
||||
textColor={iconTextColor}
|
||||
backgroundColor={iconBackgroundColor}
|
||||
testID={`${teamListItemTestId}.team_icon`}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
style={styles.text}
|
||||
style={[styles.text, {color: textColor}]}
|
||||
numberOfLines={1}
|
||||
>{team.displayName}</Text>
|
||||
>
|
||||
{displayName}
|
||||
</Text>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {View, Text} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
@@ -10,64 +10,6 @@ import {useTheme} from '@context/theme';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
lastIconUpdate: number;
|
||||
displayName: string;
|
||||
selected: boolean;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
export default function TeamIcon({id, lastIconUpdate, displayName, selected, testID}: Props) {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const ref = useRef<View>(null);
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const serverUrl = useServerUrl();
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
|
||||
useEffect(() =>
|
||||
setImageError(false)
|
||||
, [id, lastIconUpdate]);
|
||||
|
||||
const handleImageError = useCallback(() => {
|
||||
if (ref.current) {
|
||||
setImageError(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
let teamIconContent;
|
||||
if (imageError || !lastIconUpdate) {
|
||||
teamIconContent = (
|
||||
<Text
|
||||
style={styles.text}
|
||||
testID={`${testID}.display_name_abbreviation`}
|
||||
>
|
||||
{displayName?.substring(0, 2).toUpperCase()}
|
||||
</Text>
|
||||
);
|
||||
} else {
|
||||
teamIconContent = (
|
||||
<FastImage
|
||||
style={styles.image}
|
||||
source={{uri: `${serverUrl}${client.getTeamIconUrl(id, lastIconUpdate)}`}}
|
||||
onError={handleImageError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={selected ? styles.containerSelected : styles.container}
|
||||
ref={ref}
|
||||
testID={testID}
|
||||
>
|
||||
{teamIconContent}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
@@ -103,3 +45,85 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
lastIconUpdate: number;
|
||||
displayName: string;
|
||||
selected: boolean;
|
||||
backgroundColor?: string;
|
||||
textColor?: string;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
export default function TeamIcon({
|
||||
id,
|
||||
lastIconUpdate,
|
||||
displayName,
|
||||
selected,
|
||||
textColor,
|
||||
backgroundColor,
|
||||
testID,
|
||||
}: Props) {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const ref = useRef<View>(null);
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const serverUrl = useServerUrl();
|
||||
let client = null;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (err) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
useEffect(() =>
|
||||
setImageError(false)
|
||||
, [id, lastIconUpdate]);
|
||||
|
||||
const handleImageError = useCallback(() => {
|
||||
if (ref.current) {
|
||||
setImageError(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const containerStyle = useMemo(() => {
|
||||
if (selected) {
|
||||
return backgroundColor ? [styles.containerSelected, {backgroundColor}] : styles.containerSelected;
|
||||
}
|
||||
|
||||
return backgroundColor ? [styles.container, {backgroundColor}] : styles.container;
|
||||
}, [styles, backgroundColor, selected]);
|
||||
|
||||
let teamIconContent;
|
||||
if (imageError || !lastIconUpdate || !client) {
|
||||
teamIconContent = (
|
||||
<Text
|
||||
style={textColor ? [styles.text, {color: textColor}] : styles.text}
|
||||
testID={`${testID}.display_name_abbreviation`}
|
||||
>
|
||||
{displayName?.substring(0, 2).toUpperCase()}
|
||||
</Text>
|
||||
);
|
||||
} else {
|
||||
teamIconContent = (
|
||||
<FastImage
|
||||
style={styles.image}
|
||||
source={{uri: `${serverUrl}${client.getTeamIconUrl(id, lastIconUpdate)}`}}
|
||||
onError={handleImageError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={containerStyle}
|
||||
ref={ref}
|
||||
testID={testID}
|
||||
>
|
||||
{teamIconContent}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export const CHANNEL_EDIT = 'ChannelEdit';
|
||||
export const CODE = 'Code';
|
||||
export const CREATE_DIRECT_MESSAGE = 'CreateDirectMessage';
|
||||
export const CREATE_OR_EDIT_CHANNEL = 'CreateOrEditChannel';
|
||||
export const CREATE_TEAM = 'CreateTeam';
|
||||
export const CUSTOM_STATUS = 'CustomStatus';
|
||||
export const CUSTOM_STATUS_CLEAR_AFTER = 'CustomStatusClearAfter';
|
||||
export const EDIT_POST = 'EditPost';
|
||||
@@ -31,6 +32,7 @@ export const LATEX = 'Latex';
|
||||
export const LOGIN = 'Login';
|
||||
export const MENTIONS = 'Mentions';
|
||||
export const MFA = 'MFA';
|
||||
export const SELECT_TEAM = 'SelectTeam';
|
||||
export const PARTICIPANTS_LIST = 'ParticipantsList';
|
||||
export const PERMALINK = 'Permalink';
|
||||
export const POST_OPTIONS = 'PostOptions';
|
||||
@@ -59,6 +61,7 @@ export default {
|
||||
CHANNEL_DETAILS,
|
||||
CODE,
|
||||
CREATE_DIRECT_MESSAGE,
|
||||
CREATE_TEAM,
|
||||
CUSTOM_STATUS_CLEAR_AFTER,
|
||||
CUSTOM_STATUS,
|
||||
EDIT_POST,
|
||||
@@ -76,6 +79,7 @@ export default {
|
||||
LOGIN,
|
||||
MENTIONS,
|
||||
MFA,
|
||||
SELECT_TEAM,
|
||||
PARTICIPANTS_LIST,
|
||||
PERMALINK,
|
||||
POST_OPTIONS,
|
||||
|
||||
@@ -11,7 +11,8 @@ import DatabaseManager from '@database/manager';
|
||||
import {getActiveServerUrl, getServerCredentials, removeServerCredentials} from '@init/credentials';
|
||||
import {getThemeForCurrentTeam} from '@queries/servers/preference';
|
||||
import {getCurrentUserId} from '@queries/servers/system';
|
||||
import {goToScreen, resetToHome, resetToSelectServer} from '@screens/navigation';
|
||||
import {queryMyTeams} from '@queries/servers/team';
|
||||
import {goToScreen, resetToHome, resetToSelectServer, resetToTeams} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {DeepLinkChannel, DeepLinkDM, DeepLinkGM, DeepLinkPermalink, DeepLinkType, DeepLinkWithData, LaunchProps, LaunchType} from '@typings/launch';
|
||||
import {convertToNotificationData} from '@utils/notification';
|
||||
@@ -112,6 +113,7 @@ const launchApp = async (props: LaunchProps, resetNavigation = true) => {
|
||||
}
|
||||
|
||||
launchToHome({...props, launchType, serverUrl});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -142,9 +144,23 @@ const launchToHome = async (props: LaunchProps) => {
|
||||
break;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Launch app in Home screen');
|
||||
resetToHome(props);
|
||||
let nTeams = 0;
|
||||
if (props.serverUrl) {
|
||||
const database = DatabaseManager.serverDatabases[props.serverUrl]?.database;
|
||||
if (database) {
|
||||
nTeams = await queryMyTeams(database).fetchCount();
|
||||
}
|
||||
}
|
||||
|
||||
if (nTeams) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Launch app in Home screen');
|
||||
resetToHome(props);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Launch app in Select Teams screen');
|
||||
resetToTeams();
|
||||
}
|
||||
};
|
||||
|
||||
const launchToServer = (props: LaunchProps, resetNavigation: Boolean) => {
|
||||
|
||||
@@ -29,18 +29,18 @@ export const queryCategoriesByTeamIds = (database: Database, teamIds: string[])
|
||||
return database.get<CategoryModel>(CATEGORY).query(Q.where('team_id', Q.oneOf(teamIds)));
|
||||
};
|
||||
|
||||
export const prepareCategories = (operator: ServerDataOperator, categories: CategoryWithChannels[]) => {
|
||||
export const prepareCategories = (operator: ServerDataOperator, categories?: CategoryWithChannels[]) => {
|
||||
return operator.handleCategories({categories, prepareRecordsOnly: true});
|
||||
};
|
||||
|
||||
export async function prepareCategoryChannels(
|
||||
operator: ServerDataOperator,
|
||||
categories: CategoryWithChannels[],
|
||||
categories?: CategoryWithChannels[],
|
||||
): Promise<CategoryChannelModel[]> {
|
||||
try {
|
||||
const categoryChannels: CategoryChannel[] = [];
|
||||
|
||||
categories.forEach((category) => {
|
||||
categories?.forEach((category) => {
|
||||
category.channel_ids.forEach((channelId, index) => {
|
||||
categoryChannels.push({
|
||||
id: makeCategoryChannelId(category.team_id, channelId),
|
||||
|
||||
@@ -9,6 +9,9 @@ import {Database as DatabaseConstants, General, Permissions} from '@constants';
|
||||
import {isDMorGM} from '@utils/channel';
|
||||
import {hasPermission} from '@utils/role';
|
||||
|
||||
import {observeChannel, observeMyChannel} from './channel';
|
||||
import {observeMyTeam} from './team';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type RoleModel from '@typings/database/models/servers/role';
|
||||
@@ -35,8 +38,9 @@ export const queryRolesByNames = (database: Database, names: string[]) => {
|
||||
};
|
||||
|
||||
export function observePermissionForChannel(channel: ChannelModel, user: UserModel, permission: string, defaultValue: boolean) {
|
||||
const myChannel = channel.membership.observe();
|
||||
const myTeam = channel.teamId ? channel.team.observe().pipe(switchMap((t) => (t ? t.myTeam.observe() : of$(undefined)))) : of$(undefined);
|
||||
const database = channel.database;
|
||||
const myChannel = observeMyChannel(database, channel.id);
|
||||
const myTeam = channel.teamId ? observeMyTeam(database, channel.teamId) : of$(undefined);
|
||||
|
||||
return combineLatest([myChannel, myTeam]).pipe(switchMap(([mc, mt]) => {
|
||||
const rolesArray = [...user.roles.split(' ')];
|
||||
@@ -46,32 +50,35 @@ export function observePermissionForChannel(channel: ChannelModel, user: UserMod
|
||||
if (mt) {
|
||||
rolesArray.push(...mt.roles.split(' '));
|
||||
}
|
||||
return queryRolesByNames(user.database, rolesArray).observe().pipe(
|
||||
return queryRolesByNames(database, rolesArray).observeWithColumns(['permissions']).pipe(
|
||||
switchMap((r) => of$(hasPermission(r, permission, defaultValue))),
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
export function observePermissionForTeam(team: TeamModel, user: UserModel, permission: string, defaultValue: boolean) {
|
||||
return team.myTeam.observe().pipe(switchMap((myTeam) => {
|
||||
const rolesArray = [...user.roles.split(' ')];
|
||||
const database = team.database;
|
||||
return observeMyTeam(database, team.id).pipe(
|
||||
switchMap((myTeam) => {
|
||||
const rolesArray = [...user.roles.split(' ')];
|
||||
|
||||
if (myTeam) {
|
||||
rolesArray.push(...myTeam.roles.split(' '));
|
||||
}
|
||||
if (myTeam) {
|
||||
rolesArray.push(...myTeam.roles.split(' '));
|
||||
}
|
||||
|
||||
return queryRolesByNames(user.database, rolesArray).observe().pipe(
|
||||
switchMap((roles) => of$(hasPermission(roles, permission, defaultValue))),
|
||||
);
|
||||
}));
|
||||
return queryRolesByNames(database, rolesArray).observeWithColumns(['permissions']).pipe(
|
||||
switchMap((roles) => of$(hasPermission(roles, permission, defaultValue))),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function observePermissionForPost(post: PostModel, user: UserModel, permission: string, defaultValue: boolean) {
|
||||
return post.channel.observe().pipe(switchMap((c) => (c ? observePermissionForChannel(c, user, permission, defaultValue) : of$(defaultValue))));
|
||||
return observeChannel(post.database, post.channelId).pipe(switchMap((c) => (c ? observePermissionForChannel(c, user, permission, defaultValue) : of$(defaultValue))));
|
||||
}
|
||||
|
||||
export function observeCanManageChannelMembers(post: PostModel, user: UserModel) {
|
||||
return post.channel.observe().pipe((switchMap((c) => {
|
||||
return observeChannel(post.database, post.channelId).pipe((switchMap((c) => {
|
||||
if (!c || c.deleteAt !== 0 || isDMorGM(c) || c.name === General.DEFAULT_CHANNEL) {
|
||||
return of$(false);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {DEFAULT_LOCALE} from '@i18n';
|
||||
import {prepareDeleteCategory} from './categories';
|
||||
import {prepareDeleteChannel, getDefaultChannelForTeam, observeMyChannelMentionCount} from './channel';
|
||||
import {queryPreferencesByCategoryAndName} from './preference';
|
||||
import {patchTeamHistory, getConfig, getTeamHistory, observeCurrentTeamId} from './system';
|
||||
import {patchTeamHistory, getConfig, getTeamHistory, observeCurrentTeamId, getCurrentTeamId} from './system';
|
||||
import {observeThreadMentionCount} from './thread';
|
||||
import {getCurrentUser} from './user';
|
||||
|
||||
@@ -29,6 +29,15 @@ const {
|
||||
TEAM_CHANNEL_HISTORY,
|
||||
} = DatabaseConstants.MM_TABLES.SERVER;
|
||||
|
||||
export const getCurrentTeam = async (database: Database) => {
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
if (currentTeamId) {
|
||||
return getTeamById(database, currentTeamId);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Saves channels to team history & excludes & GLOBAL_THREADS from it
|
||||
export const addChannelToTeamHistory = async (operator: ServerDataOperator, teamId: string, channelId: string, prepareRecordsOnly = false) => {
|
||||
let tch: TeamChannelHistory|undefined;
|
||||
|
||||
@@ -36,7 +36,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const roles = currentUserId.pipe(
|
||||
switchMap((id) => observeUser(database, id)),
|
||||
switchMap((u) => (u ? of$(u.roles.split(' ')) : of$([]))),
|
||||
switchMap((values) => queryRolesByNames(database, values).observe()),
|
||||
switchMap((values) => queryRolesByNames(database, values).observeWithColumns(['permissions'])),
|
||||
);
|
||||
|
||||
const canCreateChannels = roles.pipe(switchMap((r) => of$(hasPermission(r, Permissions.CREATE_PUBLIC_CHANNEL, false))));
|
||||
|
||||
@@ -30,7 +30,7 @@ const enhanced = withObservables(['channelId'], ({channelId, database}: {channel
|
||||
if (memberRoles) {
|
||||
combinedRoles.push(...memberRoles);
|
||||
}
|
||||
return queryRolesByNames(database, combinedRoles).observe();
|
||||
return queryRolesByNames(database, combinedRoles).observeWithColumns(['permissions']);
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -227,78 +227,55 @@ exports[`components/categories_list should render channels error 1`] = `
|
||||
There was a problem loading content for this team.
|
||||
</Text>
|
||||
<View
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderRadius": 4,
|
||||
"flex": 0,
|
||||
"height": 48,
|
||||
"justifyContent": "center",
|
||||
"marginTop": 24,
|
||||
"opacity": 1,
|
||||
"paddingHorizontal": 24,
|
||||
"paddingVertical": 14,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"marginTop": 24,
|
||||
"alignItems": "center",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontWeight": "600",
|
||||
"justifyContent": "center",
|
||||
"padding": 1,
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
Object {
|
||||
"fontSize": 16,
|
||||
"lineHeight": 18,
|
||||
"marginTop": 1,
|
||||
},
|
||||
Object {
|
||||
"color": "#1c58d9",
|
||||
},
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"borderRadius": 4,
|
||||
"flex": 0,
|
||||
"justifyContent": "center",
|
||||
},
|
||||
Object {
|
||||
"height": 48,
|
||||
"paddingHorizontal": 24,
|
||||
"paddingVertical": 14,
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
},
|
||||
],
|
||||
]
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontWeight": "600",
|
||||
"justifyContent": "center",
|
||||
"padding": 1,
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
Object {
|
||||
"fontSize": 16,
|
||||
"lineHeight": 16,
|
||||
"marginTop": 1,
|
||||
},
|
||||
Object {
|
||||
"color": "#1c58d9",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
Retry
|
||||
</Text>
|
||||
</View>
|
||||
Retry
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -340,20 +317,75 @@ exports[`components/categories_list should render team error 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.64)",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontSize": 11,
|
||||
"fontWeight": "600",
|
||||
"lineHeight": 16,
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"justifyContent": "space-between",
|
||||
}
|
||||
}
|
||||
testID="channel_list_header.server_display_name"
|
||||
>
|
||||
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"justifyContent": "space-between",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.64)",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontSize": 14,
|
||||
"fontWeight": "600",
|
||||
"lineHeight": 20,
|
||||
}
|
||||
}
|
||||
testID="channel_list_header.team_display_name"
|
||||
>
|
||||
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
testID="channel_list_header.plus.button"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.64)",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontSize": 14,
|
||||
"fontWeight": "600",
|
||||
"lineHeight": 20,
|
||||
}
|
||||
}
|
||||
testID="channel_list_header.team_display_name"
|
||||
>
|
||||
Log out
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
accessible={true}
|
||||
@@ -476,78 +508,55 @@ exports[`components/categories_list should render team error 1`] = `
|
||||
There was a problem loading content for this server.
|
||||
</Text>
|
||||
<View
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderRadius": 4,
|
||||
"flex": 0,
|
||||
"height": 48,
|
||||
"justifyContent": "center",
|
||||
"marginTop": 24,
|
||||
"opacity": 1,
|
||||
"paddingHorizontal": 24,
|
||||
"paddingVertical": 14,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"marginTop": 24,
|
||||
"alignItems": "center",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontWeight": "600",
|
||||
"justifyContent": "center",
|
||||
"padding": 1,
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
Object {
|
||||
"fontSize": 16,
|
||||
"lineHeight": 18,
|
||||
"marginTop": 1,
|
||||
},
|
||||
Object {
|
||||
"color": "#1c58d9",
|
||||
},
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"borderRadius": 4,
|
||||
"flex": 0,
|
||||
"justifyContent": "center",
|
||||
},
|
||||
Object {
|
||||
"height": 48,
|
||||
"paddingHorizontal": 24,
|
||||
"paddingVertical": 14,
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
},
|
||||
],
|
||||
]
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontWeight": "600",
|
||||
"justifyContent": "center",
|
||||
"padding": 1,
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
Object {
|
||||
"fontSize": 16,
|
||||
"lineHeight": 16,
|
||||
"marginTop": 1,
|
||||
},
|
||||
Object {
|
||||
"color": "#1c58d9",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
Retry
|
||||
</Text>
|
||||
</View>
|
||||
Retry
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -7,14 +7,16 @@ import {Text, View} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {logout} from '@actions/remote/session';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {ITEM_HEIGHT} from '@components/slide_up_panel_item';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {useServerDisplayName} from '@context/server';
|
||||
import {useServerDisplayName, useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {bottomSheet} from '@screens/navigation';
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
import {alertServerLogout} from '@utils/server';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
@@ -61,6 +63,16 @@ const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
color: changeOpacity(theme.sidebarText, 0.8),
|
||||
fontSize: 18,
|
||||
},
|
||||
noTeamHeadingStyles: {
|
||||
color: changeOpacity(theme.sidebarText, 0.64),
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
},
|
||||
noTeamHeaderRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
height: 40,
|
||||
},
|
||||
}));
|
||||
|
||||
const ChannelListHeader = ({canCreateChannels, canJoinChannels, displayName, iconPad, onHeaderPress}: Props) => {
|
||||
@@ -74,6 +86,7 @@ const ChannelListHeader = ({canCreateChannels, canJoinChannels, displayName, ico
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
marginLeft: withTiming(marginLeft.value, {duration: 350}),
|
||||
}), []);
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
useEffect(() => {
|
||||
marginLeft.value = iconPad ? 44 : 0;
|
||||
@@ -108,51 +121,87 @@ const ChannelListHeader = ({canCreateChannels, canJoinChannels, displayName, ico
|
||||
});
|
||||
}, [intl, insets, isTablet, theme]);
|
||||
|
||||
return (
|
||||
<Animated.View style={animatedStyle}>
|
||||
{Boolean(displayName) &&
|
||||
<View style={styles.headerRow}>
|
||||
<TouchableWithFeedback
|
||||
onPress={onHeaderPress}
|
||||
type='opacity'
|
||||
>
|
||||
<View style={styles.headerRow}>
|
||||
<Text
|
||||
style={styles.headingStyles}
|
||||
testID='channel_list_header.team_display_name'
|
||||
>
|
||||
{displayName}
|
||||
</Text>
|
||||
<View
|
||||
style={styles.chevronButton}
|
||||
testID='channel_list_header.chevron.button'
|
||||
>
|
||||
<CompassIcon
|
||||
style={styles.chevronIcon}
|
||||
name={'chevron-down'}
|
||||
/>
|
||||
const onLogoutPress = useCallback(() => {
|
||||
alertServerLogout(serverDisplayName, () => logout(serverUrl), intl);
|
||||
}, []);
|
||||
|
||||
let header;
|
||||
if (displayName) {
|
||||
header = (
|
||||
<>
|
||||
<View style={styles.headerRow}>
|
||||
<TouchableWithFeedback
|
||||
onPress={onHeaderPress}
|
||||
type='opacity'
|
||||
>
|
||||
<View style={styles.headerRow}>
|
||||
<Text
|
||||
style={styles.headingStyles}
|
||||
testID='channel_list_header.team_display_name'
|
||||
>
|
||||
{displayName}
|
||||
</Text>
|
||||
<View
|
||||
style={styles.chevronButton}
|
||||
testID='channel_list_header.chevron.button'
|
||||
>
|
||||
<CompassIcon
|
||||
style={styles.chevronIcon}
|
||||
name={'chevron-down'}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
</TouchableWithFeedback>
|
||||
<TouchableWithFeedback
|
||||
onPress={onPress}
|
||||
style={styles.plusButton}
|
||||
testID='channel_list_header.plus.button'
|
||||
type='opacity'
|
||||
>
|
||||
<CompassIcon
|
||||
style={styles.plusIcon}
|
||||
name={'plus'}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
<Text
|
||||
style={styles.subHeadingStyles}
|
||||
testID='channel_list_header.server_display_name'
|
||||
>
|
||||
{serverDisplayName}
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
header = (
|
||||
<View style={styles.noTeamHeaderRow}>
|
||||
<View style={styles.noTeamHeaderRow}>
|
||||
<Text
|
||||
style={styles.noTeamHeadingStyles}
|
||||
testID='channel_list_header.team_display_name'
|
||||
>
|
||||
{serverDisplayName}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableWithFeedback
|
||||
onPress={onPress}
|
||||
style={styles.plusButton}
|
||||
onPress={onLogoutPress}
|
||||
testID='channel_list_header.plus.button'
|
||||
type='opacity'
|
||||
>
|
||||
<CompassIcon
|
||||
style={styles.plusIcon}
|
||||
name={'plus'}
|
||||
/>
|
||||
<Text
|
||||
style={styles.noTeamHeadingStyles}
|
||||
testID='channel_list_header.team_display_name'
|
||||
>
|
||||
{intl.formatMessage({id: 'account.logout', defaultMessage: 'Log out'})}
|
||||
</Text>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
}
|
||||
<Text
|
||||
style={styles.subHeadingStyles}
|
||||
testID='channel_list_header.server_display_name'
|
||||
>
|
||||
{serverDisplayName}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Animated.View style={animatedStyle}>
|
||||
{header}
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {useManagedConfig} from '@mattermost/react-native-emm';
|
||||
import {useIsFocused, useRoute} from '@react-navigation/native';
|
||||
import React from 'react';
|
||||
import React, {useEffect} from 'react';
|
||||
import {StyleSheet} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated';
|
||||
import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
@@ -12,6 +12,7 @@ import FreezeScreen from '@components/freeze_screen';
|
||||
import TeamSidebar from '@components/team_sidebar';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {resetToTeams} from '@screens/navigation';
|
||||
|
||||
import AdditionalTabletView from './additional_tablet_view';
|
||||
import CategoriesList from './categories_list';
|
||||
@@ -28,9 +29,6 @@ type ChannelProps = {
|
||||
const edges: Edge[] = ['bottom', 'left', 'right'];
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
@@ -69,6 +67,12 @@ const ChannelListScreen = (props: ChannelProps) => {
|
||||
return {height: insets.top, backgroundColor: theme.sidebarBg};
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.teamsCount) {
|
||||
resetToTeams();
|
||||
}
|
||||
}, [Boolean(props.teamsCount)]);
|
||||
|
||||
return (
|
||||
<FreezeScreen freeze={!isFocused}>
|
||||
{<Animated.View style={top}/>}
|
||||
|
||||
@@ -16,7 +16,7 @@ import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
currentTeamId: observeCurrentTeamId(database),
|
||||
isCRTEnabled: observeIsCRTEnabled(database),
|
||||
teamsCount: queryMyTeams(database).observeCount(),
|
||||
teamsCount: queryMyTeams(database).observeCount(false),
|
||||
channelsCount: queryAllMyChannel(database).observeCount(),
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
|
||||
import {IntlShape, useIntl} from 'react-intl';
|
||||
import {StyleSheet} from 'react-native';
|
||||
|
||||
@@ -48,7 +48,11 @@ const sortServers = (servers: ServersModel[], intl: IntlShape) => {
|
||||
});
|
||||
};
|
||||
|
||||
export default function Servers() {
|
||||
export type ServersRef = {
|
||||
openServers: () => void;
|
||||
}
|
||||
|
||||
const Servers = React.forwardRef<ServersRef>((props, ref) => {
|
||||
const intl = useIntl();
|
||||
const [total, setTotal] = useState<UnreadMessages>({mentions: 0, unread: false});
|
||||
const registeredServers = useRef<ServersModel[]|undefined>();
|
||||
@@ -136,6 +140,10 @@ export default function Servers() {
|
||||
}
|
||||
}, [isTablet, theme]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
openServers: onPress,
|
||||
}), [onPress]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = subscribeAllServers(serversObserver);
|
||||
|
||||
@@ -157,5 +165,8 @@ export default function Servers() {
|
||||
testID={'channel_list.servers.server_icon'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Servers.displayName = 'Servers';
|
||||
|
||||
export default Servers;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {enableFreeze, enableScreens} from 'react-native-screens';
|
||||
|
||||
import {Events, Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {alertTeamRemove} from '@utils/navigation';
|
||||
import {notificationError} from '@utils/notification';
|
||||
|
||||
import Account from './account';
|
||||
@@ -47,6 +48,16 @@ export default function HomeScreen(props: HomeProps) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = DeviceEventEmitter.addListener(Events.LEAVE_TEAM, (displayName: string) => {
|
||||
alertTeamRemove(displayName, intl);
|
||||
});
|
||||
|
||||
return () => {
|
||||
listener.remove();
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<NavigationContainer
|
||||
theme={{
|
||||
|
||||
@@ -141,6 +141,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
||||
case Screens.MFA:
|
||||
screen = withIntl(require('@screens/mfa').default);
|
||||
break;
|
||||
case Screens.SELECT_TEAM:
|
||||
screen = withServerDatabase(require('@screens/select_team').default);
|
||||
break;
|
||||
case Screens.PERMALINK:
|
||||
screen = withServerDatabase(require('@screens/permalink').default);
|
||||
break;
|
||||
|
||||
@@ -16,7 +16,7 @@ import Loading from '@components/loading';
|
||||
import {FORGOT_PASSWORD, MFA} from '@constants/screens';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {t} from '@i18n';
|
||||
import {goToScreen, loginAnimationOptions, resetToHome} from '@screens/navigation';
|
||||
import {goToScreen, loginAnimationOptions, resetToHome, resetToTeams} from '@screens/navigation';
|
||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
@@ -129,8 +129,7 @@ const LoginForm = ({config, extra, keyboardAwareRef, numberSSOs, serverDisplayNa
|
||||
const result: LoginActionResponse = await login(serverUrl!, {serverDisplayName, loginId: loginId.toLowerCase(), password, config, license});
|
||||
if (checkLoginResponse(result)) {
|
||||
if (!result.hasTeams && !result.error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('TODO: GO TO NO TEAMS');
|
||||
resetToTeams();
|
||||
return;
|
||||
}
|
||||
goToHome(result.time || 0, result.error as never);
|
||||
|
||||
@@ -275,35 +275,11 @@ export function resetToSelectServer(passProps: LaunchProps) {
|
||||
});
|
||||
}
|
||||
|
||||
export function resetToTeams(name: string, title: string, passProps = {}, options = {}) {
|
||||
export function resetToTeams() {
|
||||
const theme = getThemeFromState();
|
||||
const isDark = tinyColor(theme.sidebarBg).isDark();
|
||||
StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
|
||||
|
||||
const defaultOptions = {
|
||||
layout: {
|
||||
componentBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
statusBar: {
|
||||
visible: true,
|
||||
backgroundColor: theme.sidebarBg,
|
||||
},
|
||||
topBar: {
|
||||
visible: true,
|
||||
title: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
text: title,
|
||||
},
|
||||
backButton: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
title: '',
|
||||
},
|
||||
background: {
|
||||
color: theme.sidebarBg,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
EphemeralStore.clearNavigationComponents();
|
||||
|
||||
Navigation.setRoot({
|
||||
@@ -311,10 +287,28 @@ export function resetToTeams(name: string, title: string, passProps = {}, option
|
||||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
id: name,
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
id: Screens.SELECT_TEAM,
|
||||
name: Screens.SELECT_TEAM,
|
||||
options: {
|
||||
layout: {
|
||||
componentBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
statusBar: {
|
||||
visible: true,
|
||||
backgroundColor: theme.sidebarBg,
|
||||
},
|
||||
topBar: {
|
||||
visible: false,
|
||||
height: 0,
|
||||
background: {
|
||||
color: theme.sidebarBg,
|
||||
},
|
||||
backButton: {
|
||||
visible: false,
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
85
app/screens/select_team/add_team_item.tsx
Normal file
85
app/screens/select_team/add_team_item.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
// 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 {Text, 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 {goToScreen} from '@screens/navigation';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
height: 64,
|
||||
marginBottom: 2,
|
||||
},
|
||||
touchable: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
borderRadius: 4,
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
},
|
||||
text: {
|
||||
color: theme.sidebarText,
|
||||
marginLeft: 16,
|
||||
...typography('Body', 200),
|
||||
},
|
||||
icon_container_container: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
icon_container: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: changeOpacity(theme.sidebarText, 0.16),
|
||||
borderRadius: 10,
|
||||
},
|
||||
icon: {
|
||||
color: theme.sidebarText,
|
||||
fontSize: 24,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default function AddTeamItem() {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const intl = useIntl();
|
||||
const onPress = useCallback(async () => {
|
||||
// TODO https://mattermost.atlassian.net/browse/MM-43622
|
||||
goToScreen(Screens.CREATE_TEAM, 'Create team');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<TouchableWithFeedback
|
||||
onPress={onPress}
|
||||
type='opacity'
|
||||
style={styles.touchable}
|
||||
>
|
||||
<View style={styles.icon_container_container}>
|
||||
<View style={styles.icon_container}>
|
||||
<CompassIcon
|
||||
name='plus'
|
||||
style={styles.icon}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Text
|
||||
style={styles.text}
|
||||
numberOfLines={1}
|
||||
>{intl.formatMessage({id: 'mobile.add_team.create_team', defaultMessage: 'Create a new team'})}</Text>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
96
app/screens/select_team/header.tsx
Normal file
96
app/screens/select_team/header.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useManagedConfig} from '@mattermost/react-native-emm';
|
||||
import React, {useCallback, useMemo, useRef} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import {logout} from '@actions/remote/session';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {useServerDisplayName, useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {alertServerLogout} from '@utils/server';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import Servers, {ServersRef} from '../home/channel_list/servers';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: 20,
|
||||
marginHorizontal: 24,
|
||||
},
|
||||
text: {
|
||||
color: changeOpacity(theme.sidebarText, 0.64),
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
},
|
||||
}));
|
||||
|
||||
const MARGIN_WITH_SERVER_ICON = 66;
|
||||
|
||||
function Header() {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const serverDisplayName = useServerDisplayName();
|
||||
const serverUrl = useServerUrl();
|
||||
const managedConfig = useManagedConfig<ManagedConfig>();
|
||||
const canAddOtherServers = managedConfig?.allowOtherServers !== 'false';
|
||||
const serverButtonRef = useRef<ServersRef>(null);
|
||||
|
||||
const headerStyle = useMemo(() => ({...styles.header, marginLeft: canAddOtherServers ? MARGIN_WITH_SERVER_ICON : undefined}), [canAddOtherServers]);
|
||||
const onLogoutPress = useCallback(() => {
|
||||
alertServerLogout(serverDisplayName, () => logout(serverUrl), intl);
|
||||
}, [serverUrl, serverDisplayName]);
|
||||
|
||||
const onLabelPress = useCallback(() => {
|
||||
serverButtonRef.current?.openServers();
|
||||
}, []);
|
||||
|
||||
let serverLabel = (
|
||||
<Text
|
||||
style={styles.text}
|
||||
testID='select_team.server_display_name'
|
||||
>
|
||||
{serverDisplayName}
|
||||
</Text>
|
||||
);
|
||||
if (canAddOtherServers) {
|
||||
serverLabel = (
|
||||
<TouchableWithFeedback
|
||||
onPress={onLabelPress}
|
||||
type='opacity'
|
||||
testID='select_team.server_display_name.touchable'
|
||||
>
|
||||
{serverLabel}
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{canAddOtherServers && <Servers ref={serverButtonRef}/>}
|
||||
<View style={headerStyle}>
|
||||
{serverLabel}
|
||||
<TouchableWithFeedback
|
||||
onPress={onLogoutPress}
|
||||
testID='select_team.logout.button'
|
||||
type='opacity'
|
||||
>
|
||||
<Text
|
||||
style={styles.text}
|
||||
testID='select_team.logout.text'
|
||||
>
|
||||
{intl.formatMessage({id: 'account.logout', defaultMessage: 'Log out'})}
|
||||
</Text>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
||||
34
app/screens/select_team/index.ts
Normal file
34
app/screens/select_team/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// 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 {Permissions} from '@constants';
|
||||
import {queryRolesByNames} from '@queries/servers/role';
|
||||
import {queryMyTeams} from '@queries/servers/team';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
import {hasPermission} from '@utils/role';
|
||||
|
||||
import SelectTeam from './select_team';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
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 nTeams = queryMyTeams(database).observeCount();
|
||||
|
||||
return {
|
||||
canCreateTeams,
|
||||
nTeams,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(SelectTeam));
|
||||
106
app/screens/select_team/no_teams.tsx
Normal file
106
app/screens/select_team/no_teams.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
// 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 {StyleSheet, Text, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import Empty from '@components/illustrations/no_team';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginHorizontal: 24,
|
||||
maxWidth: 600,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
iconWrapper: {
|
||||
height: 120,
|
||||
width: 120,
|
||||
backgroundColor: changeOpacity(theme.sidebarText, 0.08),
|
||||
borderRadius: 60,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
marginTop: 24,
|
||||
textAlign: 'center',
|
||||
...typography('Heading', 800),
|
||||
},
|
||||
description: {
|
||||
color: changeOpacity(theme.sidebarText, 0.72),
|
||||
textAlign: 'center',
|
||||
marginTop: 12,
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
buttonStyle: {
|
||||
...StyleSheet.flatten(buttonBackgroundStyle(theme, 'lg', 'primary', 'default')),
|
||||
flexDirection: 'row',
|
||||
marginTop: 24,
|
||||
},
|
||||
buttonText: {
|
||||
...StyleSheet.flatten(buttonTextStyle(theme, 'lg', 'primary', 'default')),
|
||||
marginLeft: 8,
|
||||
},
|
||||
plusIcon: {
|
||||
color: theme.sidebarText,
|
||||
fontSize: 24,
|
||||
lineHeight: 22,
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
canCreateTeams: boolean;
|
||||
}
|
||||
|
||||
const NoTeams = ({
|
||||
canCreateTeams,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const intl = useIntl();
|
||||
|
||||
const onButtonPress = useCallback(async () => {
|
||||
// TODO https://mattermost.atlassian.net/browse/MM-43622
|
||||
//goToScreen(Screens.CREATE_TEAM, 'Create team');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.iconWrapper}>
|
||||
<Empty theme={theme}/>
|
||||
</View>
|
||||
<Text style={styles.title}>
|
||||
{intl.formatMessage({id: 'select_team.no_team.title', defaultMessage: 'No teams are available to join'})}
|
||||
</Text>
|
||||
<Text style={styles.description}>
|
||||
{intl.formatMessage({id: 'select_team.no_team.description', defaultMessage: 'To join a team, ask a team admin for an invite, or create your own team. You may also want to check your email inbox for an invitation.'})}
|
||||
</Text>
|
||||
{canCreateTeams &&
|
||||
<TouchableWithFeedback
|
||||
style={styles.buttonStyle}
|
||||
type={'opacity'}
|
||||
onPress={onButtonPress}
|
||||
>
|
||||
<CompassIcon
|
||||
name='plus'
|
||||
style={styles.plusIcon}
|
||||
/>
|
||||
<Text style={styles.buttonText}>{intl.formatMessage({id: 'mobile.add_team.create_team', defaultMessage: 'Create a new team'})}</Text>
|
||||
</TouchableWithFeedback>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoTeams;
|
||||
108
app/screens/select_team/select_team.tsx
Normal file
108
app/screens/select_team/select_team.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
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 {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import {resetToHome} from '../navigation';
|
||||
|
||||
import Header from './header';
|
||||
import NoTeams from './no_teams';
|
||||
import TeamList from './team_list';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.sidebarBg,
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
canCreateTeams: boolean;
|
||||
nTeams: number;
|
||||
}
|
||||
|
||||
const safeAreaEdges = ['left' as const, 'right' as const];
|
||||
const safeAreaStyle = {flex: 1};
|
||||
|
||||
const SelectTeam = ({
|
||||
canCreateTeams,
|
||||
nTeams,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const serverUrl = useServerUrl();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const insets = useSafeAreaInsets();
|
||||
const top = useAnimatedStyle(() => {
|
||||
return {height: insets.top, backgroundColor: theme.sidebarBg};
|
||||
});
|
||||
|
||||
const mounted = useRef(false);
|
||||
|
||||
const [otherTeams, setOtherTeams] = useState<Team[]>();
|
||||
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (nTeams > 0) {
|
||||
resetToHome();
|
||||
}
|
||||
}, [nTeams > 0]);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
let body;
|
||||
if (loading) {
|
||||
body = null;
|
||||
} else if (otherTeams?.length) {
|
||||
body = (
|
||||
<TeamList
|
||||
teams={otherTeams}
|
||||
canCreateTeam={canCreateTeams}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
body = (<NoTeams canCreateTeams={canCreateTeams}/>);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
mode='margin'
|
||||
edges={safeAreaEdges}
|
||||
style={safeAreaStyle}
|
||||
>
|
||||
<Animated.View style={top}/>
|
||||
<View style={styles.container}>
|
||||
<Header/>
|
||||
{body}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectTeam;
|
||||
88
app/screens/select_team/team_list.tsx
Normal file
88
app/screens/select_team/team_list.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
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 {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';
|
||||
|
||||
import AddTeamItem from './add_team_item';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.sidebarBg,
|
||||
marginHorizontal: 24,
|
||||
},
|
||||
title: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
marginTop: 40,
|
||||
...typography('Heading', 800),
|
||||
},
|
||||
description: {
|
||||
color: changeOpacity(theme.sidebarText, 0.72),
|
||||
marginTop: 12,
|
||||
marginBottom: 25,
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
separator: {
|
||||
borderColor: changeOpacity(theme.sidebarText, 0.08),
|
||||
borderTopWidth: 1,
|
||||
marginVertical: 8,
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
canCreateTeam: boolean;
|
||||
teams: Team[];
|
||||
};
|
||||
function TeamList({
|
||||
canCreateTeam,
|
||||
teams,
|
||||
}: 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'}] : styles.container;
|
||||
}, [isTablet, styles]);
|
||||
|
||||
return (
|
||||
<View style={containerStyle}>
|
||||
<View>
|
||||
<Text style={styles.title}>{intl.formatMessage({id: 'select_team.title', defaultMessage: 'Select a team'})}</Text>
|
||||
<Text style={styles.description}>{intl.formatMessage({id: 'select_team.description', defaultMessage: 'You are not yet a member of any teams. Select one below to get started.'})}</Text>
|
||||
</View>
|
||||
{canCreateTeam && (
|
||||
<>
|
||||
<AddTeamItem/>
|
||||
<View style={styles.separator}/>
|
||||
</>
|
||||
)}
|
||||
<TeamFlatList
|
||||
teams={teams}
|
||||
textColor={theme.sidebarText}
|
||||
iconBackgroundColor={changeOpacity(theme.sidebarText, 0.16)}
|
||||
iconTextColor={theme.sidebarText}
|
||||
onTeamAdded={onTeamAdded}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamList;
|
||||
@@ -13,7 +13,7 @@ import ClientError from '@client/rest/error';
|
||||
import {Screens, Sso} from '@constants';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import Background from '@screens/background';
|
||||
import {dismissModal, resetToHome} from '@screens/navigation';
|
||||
import {dismissModal, resetToHome, resetToTeams} from '@screens/navigation';
|
||||
|
||||
import SSOWithRedirectURL from './sso_with_redirect_url';
|
||||
import SSOWithWebView from './sso_with_webview';
|
||||
@@ -102,8 +102,7 @@ const SSO = ({
|
||||
return;
|
||||
}
|
||||
if (!result.hasTeams && !result.error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('GO TO NO TEAMS');
|
||||
resetToTeams();
|
||||
return;
|
||||
}
|
||||
goToHome(result.time || 0, result.error as never);
|
||||
|
||||
@@ -10,6 +10,12 @@ class EphemeralStore {
|
||||
creatingChannel = false;
|
||||
creatingDMorGMTeammates: string[] = [];
|
||||
|
||||
// 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
|
||||
// of the extra computation time. We use this to track the events that are being handled
|
||||
// and make sure we only handle one.
|
||||
private addingTeam = new Set<string>();
|
||||
|
||||
addNavigationComponentId = (componentId: string) => {
|
||||
this.addToNavigationComponentIdStack(componentId);
|
||||
this.addToAllNavigationComponentIds(componentId);
|
||||
@@ -114,6 +120,18 @@ class EphemeralStore {
|
||||
found = !this.navigationComponentIdStack.includes(componentId);
|
||||
}
|
||||
};
|
||||
|
||||
startAddingToTeam = (teamId: string) => {
|
||||
this.addingTeam.add(teamId);
|
||||
};
|
||||
|
||||
finishAddingToTeam = (teamId: string) => {
|
||||
this.addingTeam.delete(teamId);
|
||||
};
|
||||
|
||||
isAddingToTeam = (teamId: string) => {
|
||||
return this.addingTeam.has(teamId);
|
||||
};
|
||||
}
|
||||
|
||||
export default new EphemeralStore();
|
||||
|
||||
@@ -421,7 +421,7 @@ export const buttonTextStyle = (
|
||||
},
|
||||
lg: {
|
||||
fontSize: 16,
|
||||
lineHeight: 16,
|
||||
lineHeight: 18,
|
||||
marginTop: 1,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {IntlShape} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
import {Navigation, Options} from 'react-native-navigation';
|
||||
|
||||
import {Screens} from '@constants';
|
||||
@@ -10,3 +12,20 @@ export const appearanceControlledScreens = new Set([Screens.SERVER, Screens.LOGI
|
||||
export function mergeNavigationOptions(componentId: string, options: Options) {
|
||||
Navigation.mergeOptions(componentId, options);
|
||||
}
|
||||
|
||||
export async function alertTeamRemove(displayName: string, intl: IntlShape) {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'alert.removed_from_team.title',
|
||||
defaultMessage: 'Removed from team',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'alert.removed_from_team.description',
|
||||
defaultMessage: 'You have been removed from team {displayName}.',
|
||||
}, {displayName}),
|
||||
[{
|
||||
style: 'cancel',
|
||||
text: intl.formatMessage({id: 'mobile.oauth.something_wrong.okButton', defaultMessage: 'OK'}),
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
"account.settings": "Settings",
|
||||
"account.user_status.title": "User Presence",
|
||||
"account.your_profile": "Your Profile",
|
||||
"alert.removed_from_team.description": "You have been removed from team {displayName}.",
|
||||
"alert.removed_from_team.title": "Removed from team",
|
||||
"api.channel.add_guest.added": "{addedUsername} added to the channel as a guest by {username}.",
|
||||
"api.channel.add_member.added": "{addedUsername} added to the channel by {username}.",
|
||||
"api.channel.guest_join_channel.post_and_forget": "{username} joined the channel as a guest.",
|
||||
@@ -277,7 +279,7 @@
|
||||
"mobile.about.serverVersionNoBuild": "Server Version: {version}",
|
||||
"mobile.account.settings.save": "Save",
|
||||
"mobile.action_menu.select": "Select an option",
|
||||
"mobile.add_team.create_team": "Create a New Team",
|
||||
"mobile.add_team.create_team": "Create a new team",
|
||||
"mobile.add_team.join_team": "Join Another Team",
|
||||
"mobile.android.photos_permission_denied_description": "Upload photos to your server or save them to your device. Open Settings to grant {applicationName} Read and Write access to your photo library.",
|
||||
"mobile.android.photos_permission_denied_title": "{applicationName} would like to access your photos",
|
||||
@@ -525,6 +527,10 @@
|
||||
"screens.channel_details": "Channel Details",
|
||||
"screens.channel_edit_header": "Edit Channel Header",
|
||||
"search_bar.search": "Search",
|
||||
"select_team.description": "You are not yet a member of any teams. Select one below to get started.",
|
||||
"select_team.no_team.description": "To join a team, ask a team admin for an invite, or create your own team. You may also want to check your email inbox for an invitation.",
|
||||
"select_team.no_team.title": "No teams are available to join",
|
||||
"select_team.title": "Select a team",
|
||||
"server.logout.alert_description": "All associated data will be removed",
|
||||
"server.logout.alert_title": "Are you sure you want to log out of {displayName}?",
|
||||
"server.remove.alert_description": "This will remove it from your list of servers. All associated data will be removed",
|
||||
|
||||
Reference in New Issue
Block a user