Gekidou teamsidebar (#5718)
This commit is contained in:
parent
46470e0b5d
commit
e6f9b0e258
34 changed files with 2214 additions and 62 deletions
35
app/actions/local/team.ts
Normal file
35
app/actions/local/team.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {NativeModules} from 'react-native';
|
||||
|
||||
import {fetchPostsForChannel} from '@actions/remote/post';
|
||||
import {Device} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {queryCurrentTeamId, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {queryLastChannelFromTeam} from '@queries/servers/team';
|
||||
|
||||
const {MattermostManaged} = NativeModules;
|
||||
const isRunningInSplitView = MattermostManaged.isRunningInSplitView;
|
||||
|
||||
export const handleTeamChange = async (serverUrl: string, teamId: string) => {
|
||||
const {operator, database} = DatabaseManager.serverDatabases[serverUrl];
|
||||
const currentTeamId = await queryCurrentTeamId(database);
|
||||
|
||||
if (currentTeamId === teamId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let channelId = '';
|
||||
if (Device.IS_TABLET) {
|
||||
const {isSplitView} = await isRunningInSplitView();
|
||||
if (!isSplitView) {
|
||||
channelId = await queryLastChannelFromTeam(database, teamId);
|
||||
if (channelId) {
|
||||
fetchPostsForChannel(serverUrl, channelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentTeamAndChannelId(operator, teamId, channelId);
|
||||
};
|
||||
|
|
@ -5,11 +5,13 @@ import {Model} from '@nozbe/watermelondb';
|
|||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {prepareMyChannelsForTeam, queryDefaultChannelForTeam} from '@queries/servers/channel';
|
||||
import {queryWebSocketLastDisconnected} from '@queries/servers/system';
|
||||
import {prepareMyTeams, queryMyTeamById} from '@queries/servers/team';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
|
||||
import {fetchMyChannelsForTeam} from './channel';
|
||||
import {fetchPostsForUnreadChannels} from './post';
|
||||
import {fetchPostsForChannel, fetchPostsForUnreadChannels} from './post';
|
||||
import {fetchRolesIfNeeded} from './role';
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
|
|
@ -36,6 +38,7 @@ export const addUserToTeam = async (serverUrl: string, teamId: string, userId: s
|
|||
|
||||
if (!fetchOnly) {
|
||||
fetchRolesIfNeeded(serverUrl, member.roles.split(' '));
|
||||
const {channels, memberships: channelMembers} = await fetchMyChannelsForTeam(serverUrl, teamId, false, 0, true);
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (operator) {
|
||||
const myTeams: MyTeam[] = [{
|
||||
|
|
@ -46,6 +49,7 @@ export const addUserToTeam = async (serverUrl: string, teamId: string, userId: s
|
|||
const models = await Promise.all([
|
||||
operator.handleMyTeam({myTeams, prepareRecordsOnly: true}),
|
||||
operator.handleTeamMemberships({teamMemberships: [member], prepareRecordsOnly: true}),
|
||||
prepareMyChannelsForTeam(operator, teamId, channels || [], channelMembers || []),
|
||||
]);
|
||||
|
||||
if (models.length) {
|
||||
|
|
@ -54,6 +58,13 @@ export const addUserToTeam = async (serverUrl: string, teamId: string, userId: s
|
|||
await operator.batchRecords(flattenedModels);
|
||||
}
|
||||
}
|
||||
|
||||
if (await isTablet()) {
|
||||
const channel = await queryDefaultChannelForTeam(operator.database, teamId);
|
||||
if (channel) {
|
||||
fetchPostsForChannel(serverUrl, channel.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,6 +114,31 @@ export const fetchMyTeams = async (serverUrl: string, fetchOnly = false): Promis
|
|||
}
|
||||
};
|
||||
|
||||
export const fetchAllTeams = async (serverUrl: string, fetchOnly = false): Promise<MyTeamsRequest> => {
|
||||
let client;
|
||||
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) {
|
||||
await operator.handleTeam({prepareRecordsOnly: false, teams});
|
||||
}
|
||||
}
|
||||
|
||||
return {teams};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchTeamsChannelsAndUnreadPosts = async (serverUrl: string, teams: Team[], memberships: TeamMembership[], excludeTeamId?: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export default function Badge({
|
|||
|
||||
Animated.timing(opacity, {
|
||||
toValue: visible ? 1 : 0,
|
||||
duration: 150,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}).start(({finished}) => {
|
||||
if (finished && !visible) {
|
||||
|
|
|
|||
126
app/components/team_sidebar/add_team/add_team.tsx
Normal file
126
app/components/team_sidebar/add_team/add_team.tsx
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
// 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 {useWindowDimensions, View} from 'react-native';
|
||||
import {OptionsModalPresentationStyle} from 'react-native-navigation';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {Device, Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useSplitView} from '@hooks/device';
|
||||
import {showModal, showModalOverCurrentContext} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import AddTeamSlideUp from './add_team_slide_up';
|
||||
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
const ITEM_HEIGHT = 72;
|
||||
const CREATE_HEIGHT = 97;
|
||||
const HEADER_HEIGHT = 66;
|
||||
const CONTAINER_HEIGHT = 392;
|
||||
|
||||
type Props = {
|
||||
canCreateTeams: boolean;
|
||||
otherTeams: TeamModel[];
|
||||
}
|
||||
export default function AddTeam({canCreateTeams, otherTeams}: Props) {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const dimensions = useWindowDimensions();
|
||||
const intl = useIntl();
|
||||
const isSplitView = useSplitView();
|
||||
const isTablet = Device.IS_TABLET && !isSplitView;
|
||||
const maxHeight = Math.round((dimensions.height * 0.9));
|
||||
|
||||
const onPress = useCallback(preventDoubleTap(() => {
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<AddTeamSlideUp
|
||||
otherTeams={otherTeams}
|
||||
canCreateTeams={canCreateTeams}
|
||||
showTitle={!isTablet && Boolean(otherTeams.length)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
let height = CONTAINER_HEIGHT;
|
||||
if (otherTeams.length) {
|
||||
height = Math.min(maxHeight, HEADER_HEIGHT + (otherTeams.length * ITEM_HEIGHT) + (canCreateTeams ? CREATE_HEIGHT : 0));
|
||||
}
|
||||
|
||||
if (isTablet) {
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.centerChannelColor);
|
||||
const closeButtonId = 'close-join-team';
|
||||
showModal(Screens.BOTTOM_SHEET, intl.formatMessage({id: 'mobile.add_team.join_team', defaultMessage: 'Join Another Team'}), {
|
||||
closeButtonId,
|
||||
renderContent,
|
||||
snapPoints: [height, 10],
|
||||
}, {
|
||||
modalPresentationStyle: OptionsModalPresentationStyle.formSheet,
|
||||
swipeToDismiss: true,
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: closeButtonId,
|
||||
icon: closeButton,
|
||||
testID: closeButtonId,
|
||||
}],
|
||||
leftButtonColor: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
background: {
|
||||
color: theme.centerChannelBg,
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
showModalOverCurrentContext(Screens.BOTTOM_SHEET, {
|
||||
renderContent,
|
||||
snapPoints: [height, 10],
|
||||
}, {swipeToDismiss: true});
|
||||
}
|
||||
}), [canCreateTeams, otherTeams, isTablet, theme]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<TouchableWithFeedback
|
||||
onPress={onPress}
|
||||
type='opacity'
|
||||
style={styles.touchable}
|
||||
>
|
||||
<CompassIcon
|
||||
size={28}
|
||||
name='plus'
|
||||
color={changeOpacity(theme.buttonColor, 0.64)}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 0,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
borderRadius: 10,
|
||||
height: 48,
|
||||
width: 48,
|
||||
marginTop: 6,
|
||||
marginBottom: 12,
|
||||
marginHorizontal: 12,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
touchable: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
};
|
||||
});
|
||||
41
app/components/team_sidebar/add_team/add_team_slide_up.tsx
Normal file
41
app/components/team_sidebar/add_team/add_team_slide_up.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// 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 {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {Navigation} from '@constants';
|
||||
import BottomSheetContent from '@screens/bottom_sheet/content';
|
||||
|
||||
import TeamList from './team_list';
|
||||
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
type Props = {
|
||||
otherTeams: TeamModel[];
|
||||
canCreateTeams: boolean;
|
||||
showTitle?: boolean;
|
||||
}
|
||||
|
||||
export default function AddTeamSlideUp({otherTeams, canCreateTeams, showTitle = true}: Props) {
|
||||
const intl = useIntl();
|
||||
|
||||
const onPressCreate = useCallback(() => {
|
||||
//TODO Create team screen
|
||||
DeviceEventEmitter.emit(Navigation.NAVIGATION_CLOSE_MODAL);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BottomSheetContent
|
||||
buttonIcon='plus'
|
||||
buttonText={intl.formatMessage({id: 'mobile.add_team.create_team', defaultMessage: 'Create a New Team'})}
|
||||
onPress={onPressCreate}
|
||||
showButton={canCreateTeams}
|
||||
showTitle={showTitle}
|
||||
title={intl.formatMessage({id: 'mobile.add_team.join_team', defaultMessage: 'Join Another Team'})}
|
||||
>
|
||||
<TeamList teams={otherTeams}/>
|
||||
</BottomSheetContent>
|
||||
);
|
||||
}
|
||||
73
app/components/team_sidebar/add_team/no_teams.svg
Normal file
73
app/components/team_sidebar/add_team/no_teams.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 82 KiB |
93
app/components/team_sidebar/add_team/team_list.tsx
Normal file
93
app/components/team_sidebar/add_team/team_list.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {ListRenderItemInfo, View} from 'react-native';
|
||||
import {FlatList} from 'react-native-gesture-handler';
|
||||
|
||||
import {makeStyleSheetFromTheme} from '@app/utils/theme';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useTheme} from '@context/theme';
|
||||
|
||||
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[];
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
flexShrink: 1,
|
||||
},
|
||||
contentContainer: {
|
||||
marginVertical: 4,
|
||||
},
|
||||
empty: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: {
|
||||
fontFamily: 'Metropolis',
|
||||
fontSize: 20,
|
||||
color: theme.centerChannelColor,
|
||||
lineHeight: 28,
|
||||
marginTop: 16,
|
||||
},
|
||||
description: {
|
||||
fontFamily: 'Open Sans',
|
||||
fontSize: 16,
|
||||
color: theme.centerChannelColor,
|
||||
lineHeight: 24,
|
||||
marginTop: 8,
|
||||
maxWidth: 334,
|
||||
},
|
||||
}));
|
||||
|
||||
const renderTeam = ({item: t}: ListRenderItemInfo<TeamModel>) => {
|
||||
return (
|
||||
<TeamListItem
|
||||
team={t}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const keyExtractor = (item: TeamModel) => item.id;
|
||||
|
||||
export default function TeamList({teams}: Props) {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
if (teams.length) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<FlatList
|
||||
data={teams}
|
||||
renderItem={renderTeam}
|
||||
keyExtractor={keyExtractor}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.empty}>
|
||||
<Empty/>
|
||||
<FormattedText
|
||||
id='team_list.no_other_teams.title'
|
||||
defaultMessage='No additional teams to join'
|
||||
style={styles.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}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
23
app/components/team_sidebar/add_team/team_list_item/index.ts
Normal file
23
app/components/team_sidebar/add_team/team_list_item/index.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// 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 {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
|
||||
import TeamListItem from './team_list_item';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
|
||||
const {SERVER: {SYSTEM}} = MM_TABLES;
|
||||
|
||||
const withSystem = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
currentUserId: database.get<SystemModel>(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_USER_ID).pipe(
|
||||
switchMap((currentUserId) => of$(currentUserId.value))),
|
||||
}));
|
||||
|
||||
export default withDatabase(withSystem(TeamListItem));
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {DeviceEventEmitter, Text, View} from 'react-native';
|
||||
|
||||
import {addUserToTeam} from '@actions/remote/team';
|
||||
import TeamIcon from '@components/team_sidebar/team_list/team_item/team_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {Navigation} from '@constants';
|
||||
import {useServerUrl} from '@context/server_url';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
type Props = {
|
||||
team: TeamModel;
|
||||
currentUserId: string;
|
||||
}
|
||||
|
||||
export default function TeamListItem({team, currentUserId}: Props) {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const serverUrl = useServerUrl();
|
||||
const onPress = useCallback(async () => {
|
||||
await addUserToTeam(serverUrl, team.id, currentUserId);
|
||||
DeviceEventEmitter.emit(Navigation.NAVIGATION_CLOSE_MODAL);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<TouchableWithFeedback
|
||||
onPress={onPress}
|
||||
type='opacity'
|
||||
style={styles.touchable}
|
||||
>
|
||||
<View style={styles.icon_container}>
|
||||
<TeamIcon
|
||||
id={team.id}
|
||||
displayName={team.displayName}
|
||||
lastIconUpdate={team.lastTeamIconUpdatedAt}
|
||||
selected={false}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
style={styles.text}
|
||||
numberOfLines={1}
|
||||
>{team.displayName}</Text>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
height: 64,
|
||||
marginVertical: 2,
|
||||
},
|
||||
touchable: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
borderRadius: 4,
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
},
|
||||
text: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
marginLeft: 16,
|
||||
},
|
||||
icon_container: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
};
|
||||
});
|
||||
53
app/components/team_sidebar/index.ts
Normal file
53
app/components/team_sidebar/index.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
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 '@app/constants';
|
||||
import {hasPermission} from '@app/utils/role';
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
|
||||
import TeamSidebar from './team_sidebar';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type MyTeam from '@typings/database/models/servers/my_team';
|
||||
import type RoleModel from '@typings/database/models/servers/role';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
const {SERVER: {SYSTEM, MY_TEAM, TEAM, USER, ROLE}} = MM_TABLES;
|
||||
|
||||
type PropsInput = WithDatabaseArgs & {
|
||||
currentUser: UserModel;
|
||||
}
|
||||
|
||||
const withSystem = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
currentUser: database.get(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_USER_ID).pipe(
|
||||
switchMap((currentUserId: SystemModel) => database.get(USER).findAndObserve(currentUserId.value))),
|
||||
}));
|
||||
|
||||
const withTeams = withObservables([], ({currentUser, database}: PropsInput) => {
|
||||
const rolesArray = [...currentUser.roles.split(' ')];
|
||||
const roles = database.get<RoleModel>(ROLE).query(Q.where('name', Q.oneOf(rolesArray))).observe();
|
||||
const canCreateTeams = roles.pipe(switchMap((r) => of$(hasPermission(r, Permissions.CREATE_TEAM, false))));
|
||||
|
||||
const otherTeams = database.get<MyTeam>(MY_TEAM).query().observe().pipe(
|
||||
switchMap((mm) => {
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
const ids = mm.map((m) => m.id);
|
||||
return database.get<TeamModel>(TEAM).query(Q.where('id', Q.notIn(ids))).observe();
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
canCreateTeams,
|
||||
otherTeams,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(withSystem(withTeams(TeamSidebar)));
|
||||
38
app/components/team_sidebar/server_icon/server_icon.tsx
Normal file
38
app/components/team_sidebar/server_icon/server_icon.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
// TODO PLACEHOLDER
|
||||
export default function ServerIcon() {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<CompassIcon
|
||||
size={24}
|
||||
name='server-variant'
|
||||
color={changeOpacity(theme.buttonColor, 0.56)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme(() => {
|
||||
return {
|
||||
container: {
|
||||
flex: 0,
|
||||
borderRadius: 10,
|
||||
height: 60,
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
};
|
||||
});
|
||||
68
app/components/team_sidebar/team_list/index.ts
Normal file
68
app/components/team_sidebar/team_list/index.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable max-nested-callbacks */
|
||||
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$, combineLatest} from 'rxjs';
|
||||
import {switchMap, map} from 'rxjs/operators';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
import TeamList from './team_list';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type MyTeamModel from '@typings/database/models/servers/my_team';
|
||||
import type PreferenceModel from '@typings/database/models/servers/preference';
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
const {SERVER: {MY_TEAM, PREFERENCE, TEAM}} = MM_TABLES;
|
||||
|
||||
const withTeams = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const myTeams = database.get<MyTeamModel>(MY_TEAM).query().observe();
|
||||
const teamIds = database.get<TeamModel>(TEAM).query(
|
||||
Q.on(MY_TEAM, Q.where('id', Q.notEq(''))),
|
||||
).observe().pipe(
|
||||
map((ts) => ts.map((t) => ({id: t.id, displayName: t.displayName}))),
|
||||
);
|
||||
const order = database.get<PreferenceModel>(PREFERENCE).query(
|
||||
Q.where('category', Preferences.TEAMS_ORDER),
|
||||
).observe().pipe(
|
||||
switchMap((p) => (p.length ? of$(p[0].value.split(',')) : of$([]))),
|
||||
);
|
||||
const myOrderedTeams = combineLatest([myTeams, order, teamIds]).pipe(
|
||||
map(([ts, o, tids]) => {
|
||||
let ids: string[] = o;
|
||||
if (!o.length) {
|
||||
ids = tids.
|
||||
sort((a, b) => a.displayName.toLocaleLowerCase().localeCompare(b.displayName.toLocaleLowerCase())).
|
||||
map((t) => t.id);
|
||||
}
|
||||
|
||||
const indexes: {[x: string]: number} = {};
|
||||
const originalIndexes: {[x: string]: number} = {};
|
||||
ids.forEach((v, i) => {
|
||||
indexes[v] = i;
|
||||
});
|
||||
|
||||
ts.forEach((t, i) => {
|
||||
originalIndexes[t.id] = i;
|
||||
});
|
||||
|
||||
return ts.sort((a, b) => {
|
||||
if ((indexes[a.id] != null) || (indexes[b.id] != null)) {
|
||||
return (indexes[a.id] ?? -1) - (indexes[b.id] ?? -1);
|
||||
}
|
||||
return (originalIndexes[a.id] - originalIndexes[b.id]);
|
||||
});
|
||||
}),
|
||||
);
|
||||
return {
|
||||
myOrderedTeams,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(withTeams(TeamList));
|
||||
51
app/components/team_sidebar/team_list/team_item/index.ts
Normal file
51
app/components/team_sidebar/team_list/team_item/index.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {MyChannelModel} from '@database/models/server';
|
||||
|
||||
import TeamItem from './team_item';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type MyTeamModel from '@typings/database/models/servers/my_team';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
|
||||
const {SERVER: {SYSTEM, MY_CHANNEL, CHANNEL}} = MM_TABLES;
|
||||
|
||||
const withSystem = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const currentTeamId = database.get<SystemModel>(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID).pipe(
|
||||
switchMap((t) => of$(t.value)),
|
||||
);
|
||||
return {
|
||||
currentTeamId,
|
||||
};
|
||||
});
|
||||
|
||||
type WithTeamsArgs = WithDatabaseArgs & {
|
||||
myTeam: MyTeamModel;
|
||||
}
|
||||
|
||||
const withTeams = withObservables(['myTeam'], ({myTeam, database}: WithTeamsArgs) => {
|
||||
const myChannels = database.get<MyChannelModel>(MY_CHANNEL).query(Q.on(CHANNEL, Q.and(Q.where('delete_at', Q.eq(0)), Q.where('team_id', Q.eq(myTeam.id))))).observeWithColumns(['mentions_count', 'message_count']);
|
||||
const mentionCount = myChannels.pipe(
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
switchMap((val) => of$(val.reduce((acc, v) => acc + v.mentionsCount, 0))),
|
||||
);
|
||||
const hasUnreads = myChannels.pipe(
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
switchMap((val) => of$(val.reduce((acc, v) => acc + v.messageCount, 0) > 0)),
|
||||
);
|
||||
return {
|
||||
team: myTeam.team.observe(),
|
||||
mentionCount,
|
||||
hasUnreads,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(withSystem(withTeams(TeamItem)));
|
||||
102
app/components/team_sidebar/team_list/team_item/team_icon.tsx
Normal file
102
app/components/team_sidebar/team_list/team_item/team_icon.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {View, Text} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import {useServerUrl} from '@context/server_url';
|
||||
import {useTheme} from '@context/theme';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
lastIconUpdate: number;
|
||||
displayName: string;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
export default function TeamIcon({id, lastIconUpdate, displayName, selected}: 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}
|
||||
>
|
||||
{displayName?.substr(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}
|
||||
>
|
||||
{teamIconContent}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: theme.centerChannelColor,
|
||||
borderRadius: 10,
|
||||
},
|
||||
containerSelected: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: theme.centerChannelColor,
|
||||
borderRadius: 6,
|
||||
},
|
||||
text: {
|
||||
color: theme.sidebarText,
|
||||
fontFamily: 'OpenSans',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 15,
|
||||
},
|
||||
image: {
|
||||
borderRadius: 6,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
};
|
||||
});
|
||||
113
app/components/team_sidebar/team_list/team_item/team_item.tsx
Normal file
113
app/components/team_sidebar/team_list/team_item/team_item.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import {handleTeamChange} from '@actions/local/team';
|
||||
import Badge from '@components/badge';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {useServerUrl} from '@context/server_url';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import TeamIcon from './team_icon';
|
||||
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
type Props = {
|
||||
team: TeamModel;
|
||||
hasUnreads: boolean;
|
||||
mentionCount: number;
|
||||
currentTeamId: string;
|
||||
}
|
||||
|
||||
export default function TeamItem({team, hasUnreads, mentionCount, currentTeamId}: Props) {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const serverUrl = useServerUrl();
|
||||
const selected = team.id === currentTeamId;
|
||||
|
||||
const hasBadge = Boolean(mentionCount || hasUnreads);
|
||||
let mentionText = mentionCount ? mentionCount.toString() : '';
|
||||
let left = 35;
|
||||
switch (true) {
|
||||
case mentionCount > 99:
|
||||
mentionText = '99+';
|
||||
left = 20;
|
||||
break;
|
||||
case mentionCount > 9:
|
||||
left = 26;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={selected ? styles.containerSelected : styles.container}>
|
||||
<TouchableWithFeedback
|
||||
onPress={() => handleTeamChange(serverUrl, team.id)}
|
||||
type='opacity'
|
||||
>
|
||||
<TeamIcon
|
||||
displayName={team.displayName}
|
||||
id={team.id}
|
||||
lastIconUpdate={team.lastTeamIconUpdatedAt}
|
||||
selected={selected}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
<Badge
|
||||
visible={hasBadge && !selected}
|
||||
style={mentionCount > 0 ? [styles.mentions, {left}] : styles.unread}
|
||||
size={mentionCount > 0 ? 16 : 12}
|
||||
>
|
||||
{mentionText}
|
||||
</Badge>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
height: 48,
|
||||
width: 48,
|
||||
flex: 0,
|
||||
borderRadius: 10,
|
||||
marginVertical: 6,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
containerSelected: {
|
||||
height: 48,
|
||||
width: 48,
|
||||
padding: 3,
|
||||
borderWidth: 3,
|
||||
borderColor: theme.sidebarTextActiveBorder,
|
||||
borderRadius: 10,
|
||||
marginVertical: 6,
|
||||
},
|
||||
unread: {
|
||||
left: 40,
|
||||
top: 3,
|
||||
borderColor: theme.sidebarTeamBarBg,
|
||||
borderWidth: 2,
|
||||
backgroundColor: theme.mentionBg,
|
||||
width: 12,
|
||||
},
|
||||
mentions: {
|
||||
top: 1,
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
fontFamily: 'OpenSans',
|
||||
lineHeight: 15,
|
||||
borderColor: theme.sidebarTeamBarBg,
|
||||
alignItems: 'center',
|
||||
borderWidth: 2,
|
||||
minWidth: 22,
|
||||
height: 18,
|
||||
borderRadius: 9,
|
||||
backgroundColor: theme.mentionBg,
|
||||
color: theme.mentionColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
48
app/components/team_sidebar/team_list/team_list.tsx
Normal file
48
app/components/team_sidebar/team_list/team_list.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FlatList, ListRenderItemInfo, StyleSheet, View} from 'react-native';
|
||||
|
||||
import TeamItem from './team_item';
|
||||
|
||||
import type MyTeamModel from '@typings/database/models/servers/my_team';
|
||||
|
||||
type Props = {
|
||||
myOrderedTeams: MyTeamModel[];
|
||||
}
|
||||
|
||||
const keyExtractor = (item: MyTeamModel) => item.id;
|
||||
|
||||
const renderTeam = ({item: t}: ListRenderItemInfo<MyTeamModel>) => {
|
||||
return (
|
||||
<TeamItem
|
||||
myTeam={t}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default function TeamList({myOrderedTeams}: Props) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<FlatList
|
||||
bounces={false}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
data={myOrderedTeams}
|
||||
fadingEdgeLength={36}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={renderTeam}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexShrink: 1,
|
||||
},
|
||||
contentContainer: {
|
||||
alignItems: 'center',
|
||||
marginVertical: 6,
|
||||
},
|
||||
});
|
||||
65
app/components/team_sidebar/team_sidebar.tsx
Normal file
65
app/components/team_sidebar/team_sidebar.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect} from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import {fetchAllTeams} from '@actions/remote/team';
|
||||
import {TEAM_SIDEBAR_WIDTH} from '@constants/view';
|
||||
import {useServerUrl} from '@context/server_url';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import AddTeam from './add_team/add_team';
|
||||
import ServerIcon from './server_icon/server_icon';
|
||||
import TeamList from './team_list';
|
||||
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
type Props = {
|
||||
canCreateTeams: boolean;
|
||||
otherTeams: TeamModel[];
|
||||
}
|
||||
|
||||
export default function TeamSidebar({canCreateTeams, otherTeams}: Props) {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
useEffect(() => {
|
||||
fetchAllTeams(serverUrl);
|
||||
}, [serverUrl]);
|
||||
|
||||
const showAddTeam = canCreateTeams || otherTeams.length > 0;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ServerIcon/>
|
||||
<View style={styles.listContainer}>
|
||||
<TeamList/>
|
||||
{showAddTeam && (
|
||||
<AddTeam
|
||||
canCreateTeams={canCreateTeams}
|
||||
otherTeams={otherTeams}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
width: TEAM_SIDEBAR_WIDTH,
|
||||
height: '100%',
|
||||
backgroundColor: theme.sidebarBg,
|
||||
display: 'flex',
|
||||
},
|
||||
listContainer: {
|
||||
backgroundColor: theme.sidebarTeamBarBg,
|
||||
borderTopRightRadius: 12,
|
||||
flex: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -42,6 +42,7 @@ export const SMALL_BADGE_MAX_WIDTH = 26;
|
|||
export const MAX_BADGE_RIGHT_POSITION = -13;
|
||||
export const LARGE_BADGE_RIGHT_POSITION = -11;
|
||||
export const SMALL_BADGE_RIGHT_POSITION = -9;
|
||||
export const TEAM_SIDEBAR_WIDTH = 72;
|
||||
|
||||
export const TABLET = {
|
||||
SIDEBAR_WIDTH: 320,
|
||||
|
|
@ -137,4 +138,5 @@ export default {
|
|||
IOS_HORIZONTAL_LANDSCAPE: 44,
|
||||
INDICATOR_BAR_HEIGHT,
|
||||
TABLET,
|
||||
TEAM_SIDEBAR_WIDTH,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ export default class ChannelModel extends Model {
|
|||
/** A CHANNEL can contain multiple POST (relationship is 1:N) */
|
||||
[POST]: {type: 'has_many', foreignKey: 'channel_id'},
|
||||
|
||||
/** A CHANNEL can contain multiple POST (relationship is 1:N) */
|
||||
[MY_CHANNEL]: {type: 'has_many', foreignKey: 'channel_id'},
|
||||
/** A CHANNEL can be associated to one MY_CHANNEL (relationship is 1:1) */
|
||||
[MY_CHANNEL]: {type: 'has_many', foreignKey: 'id'},
|
||||
|
||||
/** A TEAM can be associated to CHANNEL (relationship is 1:N) */
|
||||
[TEAM]: {type: 'belongs_to', key: 'team_id'},
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import {Relation} from '@nozbe/watermelondb';
|
||||
import {field, relation} from '@nozbe/watermelondb/decorators';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
import Model, {Associations} from '@nozbe/watermelondb/Model';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
|
|
@ -18,6 +18,10 @@ export default class MyTeamModel extends Model {
|
|||
/** table (name) : MyTeam */
|
||||
static table = MY_TEAM;
|
||||
|
||||
static associations: Associations = {
|
||||
[TEAM]: {type: 'belongs_to', key: 'id'},
|
||||
};
|
||||
|
||||
/** roles : The different permissions that this user has in the team, concatenated together with comma to form a single string. */
|
||||
@field('roles') roles!: string;
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ export default class TeamModel extends Model {
|
|||
/** A TEAM has a 1:N relationship with GROUPS_TEAM. A TEAM can possess multiple groups */
|
||||
[GROUPS_TEAM]: {type: 'has_many', foreignKey: 'team_id'},
|
||||
|
||||
/** A TEAM can be associated to one MY_TEAM (relationship is 1:1) */
|
||||
[MY_TEAM]: {type: 'has_many', foreignKey: 'id'},
|
||||
|
||||
/** A TEAM has a 1:N relationship with SLASH_COMMAND. A TEAM can possess multiple slash commands */
|
||||
[SLASH_COMMAND]: {type: 'has_many', foreignKey: 'team_id'},
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@
|
|||
|
||||
import {Database, Model, Q, Query, Relation} from '@nozbe/watermelondb';
|
||||
|
||||
import {General, Permissions} from '@constants';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {hasPermission} from '@utils/role';
|
||||
|
||||
import {prepareDeletePost} from './post';
|
||||
import {queryRoles} from './role';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
|
@ -125,3 +128,34 @@ export const queryChannelsById = async (database: Database, channelIds: string[]
|
|||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const queryDefaultChannelForTeam = async (database: Database, teamId: string) => {
|
||||
let channel: ChannelModel|undefined;
|
||||
let canIJoinPublicChannelsInTeam = false;
|
||||
const roles = await queryRoles(database);
|
||||
|
||||
if (roles.length) {
|
||||
canIJoinPublicChannelsInTeam = hasPermission(roles, Permissions.JOIN_PUBLIC_CHANNELS, true);
|
||||
}
|
||||
|
||||
const myChannels = await database.get<ChannelModel>(CHANNEL).query(
|
||||
Q.on(MY_CHANNEL, 'id', Q.notEq('')),
|
||||
Q.and(
|
||||
Q.where('team_id', teamId),
|
||||
Q.where('delete_at', 0),
|
||||
Q.where('type', General.OPEN_CHANNEL),
|
||||
),
|
||||
Q.experimentalSortBy('display_name', Q.asc),
|
||||
).fetch();
|
||||
|
||||
const defaultChannel = myChannels.find((c) => c.name === General.DEFAULT_CHANNEL);
|
||||
const myFirstTeamChannel = myChannels[0];
|
||||
|
||||
if (defaultChannel || canIJoinPublicChannelsInTeam) {
|
||||
channel = defaultChannel;
|
||||
} else {
|
||||
channel = myFirstTeamChannel || defaultChannel;
|
||||
}
|
||||
|
||||
return channel;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {Database as DatabaseConstants, Preferences} from '@constants';
|
|||
import {getPreferenceValue} from '@helpers/api/preference';
|
||||
import {selectDefaultTeam} from '@helpers/api/team';
|
||||
|
||||
import {prepareDeleteChannel} from './channel';
|
||||
import {prepareDeleteChannel, queryDefaultChannelForTeam} from './channel';
|
||||
import {queryPreferencesByCategoryAndName} from './preference';
|
||||
import {queryConfig} from './system';
|
||||
import {queryCurrentUser} from './user';
|
||||
|
|
@ -46,6 +46,25 @@ export const addChannelToTeamHistory = async (operator: ServerDataOperator, team
|
|||
return operator.handleTeamChannelHistory({teamChannelHistories: [tch], prepareRecordsOnly});
|
||||
};
|
||||
|
||||
export const queryLastChannelFromTeam = async (database: Database, teamId: string) => {
|
||||
let channelId = '';
|
||||
|
||||
try {
|
||||
const teamChannelHistory = await database.get<TeamChannelHistoryModel>(TEAM_CHANNEL_HISTORY).find(teamId);
|
||||
if (teamChannelHistory.channelIds.length) {
|
||||
channelId = teamChannelHistory.channelIds[0];
|
||||
}
|
||||
} catch {
|
||||
// No channel history for the team
|
||||
const channel = await queryDefaultChannelForTeam(database, teamId);
|
||||
if (channel) {
|
||||
channelId = channel.id;
|
||||
}
|
||||
}
|
||||
|
||||
return channelId;
|
||||
};
|
||||
|
||||
export const prepareMyTeams = (operator: ServerDataOperator, teams: Team[], memberships: TeamMembership[]) => {
|
||||
try {
|
||||
const teamRecords = operator.handleTeam({prepareRecordsOnly: true, teams});
|
||||
|
|
|
|||
72
app/screens/bottom_sheet/button.tsx
Normal file
72
app/screens/bottom_sheet/button.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {GestureResponderEvent, Text, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
onPress?: (e: GestureResponderEvent) => void;
|
||||
icon?: string;
|
||||
text?: string;
|
||||
}
|
||||
export default function BottomSheetButton({onPress, icon, text}: Props) {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={onPress}
|
||||
type='opacity'
|
||||
style={styles.button}
|
||||
>
|
||||
{icon && (
|
||||
<View style={styles.icon_container}>
|
||||
<CompassIcon
|
||||
size={24}
|
||||
name={icon}
|
||||
color={theme.buttonColor}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{text && (
|
||||
<Text
|
||||
style={styles.text}
|
||||
>{text}</Text>
|
||||
)}
|
||||
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
button: {
|
||||
backgroundColor: theme.buttonBg,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 24,
|
||||
borderRadius: 4,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: 48,
|
||||
},
|
||||
text: {
|
||||
color: theme.buttonColor,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 16,
|
||||
lineHeight: 18,
|
||||
paddingHorizontal: 1,
|
||||
},
|
||||
icon_container: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
marginBottom: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
80
app/screens/bottom_sheet/content.tsx
Normal file
80
app/screens/bottom_sheet/content.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {GestureResponderEvent, Platform, Text, useWindowDimensions, View} from 'react-native';
|
||||
|
||||
import {Device} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useSplitView} from '@hooks/device';
|
||||
import Button from '@screens/bottom_sheet/button';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
buttonIcon?: string;
|
||||
buttonText?: string;
|
||||
children: React.ReactNode;
|
||||
onPress?: (e: GestureResponderEvent) => void;
|
||||
showButton: boolean;
|
||||
showTitle: boolean;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
titleContainer: {
|
||||
marginTop: 4,
|
||||
marginBottom: 4,
|
||||
},
|
||||
titleText: {
|
||||
color: theme.centerChannelColor,
|
||||
lineHeight: 30,
|
||||
fontSize: 25,
|
||||
fontFamily: 'OpenSans-Semibold',
|
||||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
right: 16,
|
||||
borderTopWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const BottomSheetContent = ({buttonText, buttonIcon, children, onPress, showButton, showTitle, title}: Props) => {
|
||||
const dimensions = useWindowDimensions();
|
||||
const theme = useTheme();
|
||||
const isSplitView = useSplitView();
|
||||
const isTablet = Device.IS_TABLET && !isSplitView;
|
||||
const styles = getStyleSheet(theme);
|
||||
const separatorWidth = Math.max(dimensions.width, 450);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{showTitle &&
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.titleText}>{title}</Text>
|
||||
</View>
|
||||
}
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
{showButton && (
|
||||
<>
|
||||
<View style={[styles.separator, {width: separatorWidth, marginBottom: (isTablet ? 20 : 12)}]}/>
|
||||
<Button
|
||||
onPress={onPress}
|
||||
icon={buttonIcon}
|
||||
text={buttonText}
|
||||
/>
|
||||
<View style={{paddingBottom: Platform.select({ios: (isTablet ? 20 : 32), android: 20})}}/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default BottomSheetContent;
|
||||
|
|
@ -91,14 +91,13 @@ const BottomSheet = ({closeButtonId, initialSnapIndex = 0, renderContent, snapPo
|
|||
);
|
||||
};
|
||||
|
||||
const renderContainer = () => (
|
||||
const renderContainerContent = () => (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
opacity: 1,
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: isTablet ? 20 : 16,
|
||||
paddingTop: isTablet ? 0 : 16,
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: isTablet ? 0 : 20,
|
||||
height: '100%',
|
||||
width: isTablet ? '100%' : Math.min(dimensions.width, 450),
|
||||
alignSelf: 'center',
|
||||
|
|
@ -113,7 +112,7 @@ const BottomSheet = ({closeButtonId, initialSnapIndex = 0, renderContent, snapPo
|
|||
return (
|
||||
<>
|
||||
<View style={styles.separator}/>
|
||||
{renderContainer()}
|
||||
{renderContainerContent()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -125,7 +124,7 @@ const BottomSheet = ({closeButtonId, initialSnapIndex = 0, renderContent, snapPo
|
|||
snapPoints={snapPoints}
|
||||
borderRadius={10}
|
||||
initialSnap={initialSnapIndex}
|
||||
renderContent={renderContainer}
|
||||
renderContent={renderContainerContent}
|
||||
onCloseEnd={() => dismissModal()}
|
||||
enabledBottomInitialAnimation={true}
|
||||
renderHeader={Indicator}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ const ChannelNavBar = ({channel, onPress}: ChannelNavBar) => {
|
|||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
header: {
|
||||
backgroundColor: theme.sidebarHeaderBg,
|
||||
backgroundColor: theme.sidebarBg,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
width: '100%',
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@ import {useIsFocused, useRoute} from '@react-navigation/native';
|
|||
import React from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import TeamSidebar from '@components/team_sidebar';
|
||||
import {Device, View as ViewConstants} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useSplitView} from '@hooks/device';
|
||||
|
|
@ -46,12 +47,13 @@ const ChannelListScreen = (props: ChannelProps) => {
|
|||
const showTabletLayout = Device.IS_TABLET && !isSplitView;
|
||||
const route = useRoute();
|
||||
const isFocused = useIsFocused();
|
||||
const insets = useSafeAreaInsets();
|
||||
const params = route.params as {direction: string};
|
||||
|
||||
let tabletSidebarStyle;
|
||||
if (showTabletLayout) {
|
||||
const {TABLET} = ViewConstants;
|
||||
tabletSidebarStyle = {maxWidth: TABLET.SIDEBAR_WIDTH};
|
||||
const {TABLET, TEAM_SIDEBAR_WIDTH} = ViewConstants;
|
||||
tabletSidebarStyle = {maxWidth: (TABLET.SIDEBAR_WIDTH - TEAM_SIDEBAR_WIDTH)};
|
||||
}
|
||||
|
||||
const animated = useAnimatedStyle(() => {
|
||||
|
|
@ -72,25 +74,30 @@ const ChannelListScreen = (props: ChannelProps) => {
|
|||
}, [isFocused, params]);
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={styles.flex}
|
||||
>
|
||||
<Animated.View
|
||||
style={[styles.content, animated]}
|
||||
<>
|
||||
{Boolean(insets.top) && <View style={{height: insets.top, backgroundColor: theme.sidebarBg}}/>}
|
||||
<SafeAreaView
|
||||
style={styles.content}
|
||||
edges={['bottom', 'left', 'right']}
|
||||
>
|
||||
<View style={[styles.flex, {alignItems: 'center', justifyContent: 'center'}, tabletSidebarStyle]}>
|
||||
<Text
|
||||
onPress={() => goToScreen('Channel', '', undefined, {topBar: {visible: false}})}
|
||||
style={{fontSize: 20, color: theme.centerChannelColor}}
|
||||
>
|
||||
{'Channel List'}
|
||||
</Text>
|
||||
</View>
|
||||
{showTabletLayout &&
|
||||
<Channel {...props}/>
|
||||
}
|
||||
</Animated.View>
|
||||
</SafeAreaView>
|
||||
<Animated.View
|
||||
style={[styles.content, animated]}
|
||||
>
|
||||
<TeamSidebar/>
|
||||
<View style={[styles.flex, {alignItems: 'center', justifyContent: 'center'}, tabletSidebarStyle]}>
|
||||
<Text
|
||||
onPress={() => goToScreen('Channel', '', undefined, {topBar: {visible: false}})}
|
||||
style={{fontSize: 20, color: theme.centerChannelColor}}
|
||||
>
|
||||
{'Channel List'}
|
||||
</Text>
|
||||
</View>
|
||||
{showTabletLayout &&
|
||||
<Channel {...props}/>
|
||||
}
|
||||
</Animated.View>
|
||||
</SafeAreaView>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -90,18 +90,17 @@ const Home = ({isFocused, theme}: Props) => {
|
|||
if (server.lastActiveAt) {
|
||||
const sdb = DatabaseManager.serverDatabases[serverUrl];
|
||||
if (sdb?.database) {
|
||||
const subscription = sdb.database.
|
||||
get(MY_CHANNEL).
|
||||
query(Q.on(CHANNEL, Q.where('delete_at', Q.eq(0)))).
|
||||
observeWithColumns(['mentions_count', 'message_count']).
|
||||
subscribe(unreadsSubscription.bind(undefined, serverUrl));
|
||||
if (!subscriptions.has(serverUrl)) {
|
||||
const unreads: UnreadSubscription = {
|
||||
mentions: 0,
|
||||
messages: 0,
|
||||
subscription,
|
||||
};
|
||||
subscriptions.set(serverUrl, unreads);
|
||||
unreads.subscription = sdb.database.
|
||||
get(MY_CHANNEL).
|
||||
query(Q.on(CHANNEL, Q.where('delete_at', Q.eq(0)))).
|
||||
observeWithColumns(['mentions_count', 'message_count']).
|
||||
subscribe(unreadsSubscription.bind(undefined, serverUrl));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,14 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment, {Moment} from 'moment-timezone';
|
||||
import {NativeModules} from 'react-native';
|
||||
|
||||
import {Device} from '@constants';
|
||||
import {CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES} from '@constants/custom_status';
|
||||
|
||||
const {MattermostManaged} = NativeModules;
|
||||
const isRunningInSplitView = MattermostManaged.isRunningInSplitView;
|
||||
|
||||
// isMinimumServerVersion will return true if currentVersion is equal to higher or than
|
||||
// the provided minimum version. A non-equal major version will ignore minor and dot
|
||||
// versions, and a non-equal minor version will ignore dot version.
|
||||
|
|
@ -119,3 +124,12 @@ export function getRoundedTime(value: Moment) {
|
|||
const remainder = roundedTo - diff;
|
||||
return start.add(remainder, 'm').seconds(0).milliseconds(0);
|
||||
}
|
||||
|
||||
export async function isTablet() {
|
||||
if (Device.IS_TABLET) {
|
||||
const {isSplitView} = await isRunningInSplitView();
|
||||
return !isSplitView;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,6 +148,8 @@
|
|||
"mobile.about.serverVersion": "Server Version: {version} (Build {number})",
|
||||
"mobile.about.serverVersionNoBuild": "Server Version: {version}",
|
||||
"mobile.action_menu.select": "Select an option",
|
||||
"mobile.add_team.create_team": "Create a New Team",
|
||||
"mobile.add_team.join_team": "Join Another Team",
|
||||
"mobile.components.select_server_view.connect": "Connect",
|
||||
"mobile.components.select_server_view.connecting": "Connecting...",
|
||||
"mobile.components.select_server_view.enterServerUrl": "Enter Server URL",
|
||||
|
|
@ -292,5 +294,7 @@
|
|||
"status_dropdown.set_offline": "Offline",
|
||||
"status_dropdown.set_online": "Online",
|
||||
"status_dropdown.set_ooo": "Out Of Office",
|
||||
"team_list.no_other_teams.description": "To join another team, ask a Team Admin for an invitation, or create your own team.",
|
||||
"team_list.no_other_teams.title": "No additional teams to join",
|
||||
"web.root.signup_info": "All team communication in one place, searchable and accessible anywhere"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,25 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
module.exports = {
|
||||
transformer: {
|
||||
getTransformOptions: async () => ({
|
||||
transform: {
|
||||
experimentalImportSupport: false,
|
||||
inlineRequires: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
const {getDefaultConfig} = require('metro-config');
|
||||
|
||||
module.exports = (async () => {
|
||||
const {
|
||||
resolver: {sourceExts, assetExts},
|
||||
} = await getDefaultConfig();
|
||||
return {
|
||||
transformer: {
|
||||
babelTransformerPath: require.resolve('react-native-svg-transformer'),
|
||||
getTransformOptions: async () => ({
|
||||
transform: {
|
||||
experimentalImportSupport: false,
|
||||
inlineRequires: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
resolver: {
|
||||
assetExts: assetExts.filter((ext) => ext !== 'svg'),
|
||||
sourceExts: [...sourceExts, 'svg'],
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
881
package-lock.json
generated
881
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -145,6 +145,7 @@
|
|||
"react-native-dev-menu": "4.0.2",
|
||||
"react-native-dotenv": "3.2.0",
|
||||
"react-native-storybook-loader": "2.0.4",
|
||||
"react-native-svg-transformer": "0.14.3",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"ts-jest": "27.0.5",
|
||||
"typescript": "4.4.3",
|
||||
|
|
|
|||
Loading…
Reference in a new issue