Gekidou CRT - User avatar stack (#6139)

* User avatar stack

* Fixed fetchPostThread & added observer

* Reusing the user component

* Refactor fix

* fix lint

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Anurag Shivarathri
2022-04-12 19:06:13 +05:30
committed by GitHub
parent b4b5c80629
commit deb222a01d
7 changed files with 250 additions and 3 deletions

View File

@@ -520,7 +520,8 @@ export async function fetchPostThread(serverUrl: string, postId: string, fetchOn
}
try {
const data = await client.getPostThread(postId);
const isCRTEnabled = await getIsCRTEnabled(operator.database);
const data = await client.getPostThread(postId, isCRTEnabled, isCRTEnabled);
const result = processPostsFetched(data);
if (!fetchOnly) {
const models = await operator.handlePosts({
@@ -528,7 +529,6 @@ export async function fetchPostThread(serverUrl: string, postId: string, fetchOn
actionType: ActionType.POSTS.RECEIVED_IN_THREAD,
prepareRecordsOnly: true,
});
const isCRTEnabled = await getIsCRTEnabled(operator.database);
if (isCRTEnabled) {
const threadModels = await prepareThreadsFromReceivedPosts(operator, result.posts);
if (threadModels?.length) {

View File

@@ -0,0 +1,157 @@
// 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 {StyleProp, Text, TouchableOpacity, View, ViewStyle} from 'react-native';
import FormattedText from '@components/formatted_text';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {bottomSheet} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import UserAvatar from './user_avatar';
import UsersList from './users_list';
import type UserModel from '@typings/database/models/servers/user';
const OVERFLOW_DISPLAY_LIMIT = 99;
type Props = {
users: UserModel[];
breakAt?: number;
style?: StyleProp<ViewStyle>;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
const size = 24;
const imgOverlap = -6;
return {
container: {
flexDirection: 'row',
},
firstAvatar: {
justifyContent: 'center',
alignItems: 'center',
width: size,
height: size,
borderWidth: (size / 2) + 1,
borderColor: theme.centerChannelBg,
backgroundColor: theme.centerChannelBg,
borderRadius: size / 2,
},
notFirstAvatars: {
justifyContent: 'center',
alignItems: 'center',
width: size,
height: size,
borderWidth: (size / 2) + 1,
borderColor: theme.centerChannelBg,
backgroundColor: theme.centerChannelBg,
borderRadius: size / 2,
marginLeft: imgOverlap,
},
overflowContainer: {
justifyContent: 'center',
alignItems: 'center',
width: size,
height: size,
borderRadius: size / 2,
borderWidth: 1,
borderColor: theme.centerChannelBg,
backgroundColor: theme.centerChannelBg,
marginLeft: imgOverlap,
},
overflowItem: {
justifyContent: 'center',
alignItems: 'center',
width: size,
height: size,
borderRadius: size / 2,
borderWidth: 1,
borderColor: theme.centerChannelBg,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
},
overflowText: {
fontSize: 10,
fontWeight: 'bold',
color: changeOpacity(theme.centerChannelColor, 0.64),
textAlign: 'center',
},
listHeader: {
marginBottom: 12,
},
listHeaderText: {
color: changeOpacity(theme.centerChannelColor, 0.56),
fontSize: 12,
fontWeight: '600',
},
};
});
const UserAvatarsStack = ({breakAt = 3, style: baseContainerStyle, users}: Props) => {
const theme = useTheme();
const intl = useIntl();
const isTablet = useIsTablet();
const showParticipantsList = useCallback(preventDoubleTap(() => {
const renderContent = () => (
<>
{!isTablet && (
<View style={style.listHeader}>
<FormattedText
id='mobile.participants.header'
defaultMessage={'THREAD PARTICIPANTS'}
style={style.listHeaderText}
/>
</View>
)}
<UsersList users={users}/>
</>
);
bottomSheet({
closeButtonId: 'close-set-user-status',
renderContent,
snapPoints: [(Math.min(14, users.length) + 3) * 40, 10],
title: intl.formatMessage({id: 'mobile.participants.header', defaultMessage: 'THREAD PARTICIPANTS'}),
theme,
});
}), [isTablet, theme, users]);
const displayUsers = users.slice(0, breakAt);
const overflowUsersCount = Math.min(users.length - displayUsers.length, OVERFLOW_DISPLAY_LIMIT);
const style = getStyleSheet(theme);
return (
<TouchableOpacity
onPress={showParticipantsList}
style={baseContainerStyle}
>
<View style={style.container}>
{displayUsers.map((user, index) => (
<UserAvatar
key={user.id}
style={index === 0 ? style.firstAvatar : style.notFirstAvatars}
user={user}
/>
))}
{Boolean(overflowUsersCount) && (
<View style={style.overflowContainer}>
<View style={style.overflowItem}>
<Text style={style.overflowText} >
{'+' + overflowUsersCount.toString()}
</Text>
</View>
</View>
)}
</View>
</TouchableOpacity>
);
};
export default UserAvatarsStack;

View File

@@ -0,0 +1,17 @@
// 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 UserAvatar from './user_avatar';
import type UserModel from '@typings/database/models/servers/user';
const enhanced = withObservables(['user'], ({user}: {user: UserModel}) => {
return {
user: user.observe(),
};
});
export default withDatabase(enhanced(UserAvatar));

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {StyleProp, View, ViewStyle} from 'react-native';
import ProfilePicture from '@components/profile_picture/image';
import type UserModel from '@typings/database/models/servers/user';
export type Props = {
style: StyleProp<ViewStyle>;
user: UserModel;
};
const UserAvatar = ({style, user}: Props) => {
return (
<View
key={user.id}
style={style}
>
<ProfilePicture
author={user}
size={24}
/>
</View>
);
};
export default UserAvatar;

View File

@@ -0,0 +1,35 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {StyleSheet} from 'react-native';
import UserItem from '@components/user_item';
import type UserModel from '@typings/database/models/servers/user';
type Props = {
users: UserModel[];
};
const style = StyleSheet.create({
container: {
paddingLeft: 0,
},
});
const UsersList = ({users}: Props) => {
return (
<>
{users.map((user) => (
<UserItem
key={user.id}
user={user}
containerStyle={style.container}
/>
))}
</>
);
};
export default UsersList;

View File

@@ -15,8 +15,9 @@ import {getConfig, observeConfig} from './system';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type Model from '@nozbe/watermelondb/Model';
import type ThreadModel from '@typings/database/models/servers/thread';
import type UserModel from '@typings/database/models/servers/user';
const {SERVER: {THREADS_IN_TEAM, THREAD, POST, CHANNEL}} = MM_TABLES;
const {SERVER: {CHANNEL, POST, THREAD, THREADS_IN_TEAM, THREAD_PARTICIPANT, USER}} = MM_TABLES;
export const getIsCRTEnabled = async (database: Database): Promise<boolean> => {
const config = await getConfig(database);
@@ -190,3 +191,9 @@ export const queryThreads = (database: Database, teamId?: string, onlyUnreads =
return database.get<ThreadModel>(THREAD).query(...query);
};
export const queryThreadParticipants = (database: Database, threadId: string) => {
return database.get<UserModel>(USER).query(
Q.on(THREAD_PARTICIPANT, Q.where('thread_id', threadId)),
);
};

View File

@@ -332,6 +332,7 @@
"mobile.oauth.switch_to_browser.title": "Redirecting...",
"mobile.oauth.try_again": "Try again",
"mobile.open_gm.error": "We couldn't open a group message with those users. Please check your connection and try again.",
"mobile.participants.header": "THREAD PARTICIPANTS",
"mobile.permission_denied_dismiss": "Don't Allow",
"mobile.permission_denied_retry": "Settings",
"mobile.post_info.add_reaction": "Add Reaction",