From 368f6aa08d30cfa99705290d71400bf9341f1d9c Mon Sep 17 00:00:00 2001 From: Shaz MJ Date: Tue, 22 Mar 2022 09:42:47 +1100 Subject: [PATCH 01/13] Handes visibility preference setting for dm/gm --- .../categories/body/category_body.test.tsx | 1 + .../categories/body/category_body.tsx | 18 ++++++--- .../channel_list/categories/body/index.ts | 40 +++++++++++++++---- .../channel_list/categories/categories.tsx | 5 ++- .../channel_list/categories/index.ts | 6 ++- app/constants/preferences.ts | 3 +- app/queries/servers/channel.ts | 4 ++ app/queries/servers/preference.ts | 16 ++++++++ 8 files changed, 78 insertions(+), 15 deletions(-) diff --git a/app/components/channel_list/categories/body/category_body.test.tsx b/app/components/channel_list/categories/body/category_body.test.tsx index 220ac79ae..3a32e3bf2 100644 --- a/app/components/channel_list/categories/body/category_body.test.tsx +++ b/app/components/channel_list/categories/body/category_body.test.tsx @@ -36,6 +36,7 @@ describe('components/channel_list/categories/body', () => { category={category} locale={DEFAULT_LOCALE} currentChannelId={''} + currentUserId={''} />, {database}, ); diff --git a/app/components/channel_list/categories/body/category_body.tsx b/app/components/channel_list/categories/body/category_body.tsx index 38f3787a9..c27f2ac00 100644 --- a/app/components/channel_list/categories/body/category_body.tsx +++ b/app/components/channel_list/categories/body/category_body.tsx @@ -11,19 +11,27 @@ import type CategoryModel from '@typings/database/models/servers/category'; type Props = { currentChannelId: string; sortedIds: string[]; + hiddenChannelIds: string[]; category: CategoryModel; limit: number; }; const extractKey = (item: string) => item; -const CategoryBody = ({currentChannelId, sortedIds, category, limit}: Props) => { +const CategoryBody = ({currentChannelId, sortedIds, category, hiddenChannelIds, limit}: Props) => { const ids = useMemo(() => { - if (category.type === 'direct_messages' && limit > 0) { - return sortedIds.slice(0, limit - 1); + let filteredIds = sortedIds; + + // Remove all closed gm/dms + if (hiddenChannelIds.length) { + filteredIds = sortedIds.filter((id) => !hiddenChannelIds.includes(id)); } - return sortedIds; - }, [category.type, limit]); + + if (category.type === 'direct_messages' && limit > 0) { + return filteredIds.slice(0, limit - 1); + } + return filteredIds; + }, [category.type, limit, hiddenChannelIds]); const ChannelItem = useCallback(({item}: {item: string}) => { return ( diff --git a/app/components/channel_list/categories/body/index.ts b/app/components/channel_list/categories/body/index.ts index 9acf7d37e..3ca076ef4 100644 --- a/app/components/channel_list/categories/body/index.ts +++ b/app/components/channel_list/categories/body/index.ts @@ -8,6 +8,9 @@ import {combineLatest, of as of$} from 'rxjs'; import {switchMap} from 'rxjs/operators'; import {MM_TABLES} from '@app/constants/database'; +import {queryChannelsByNames} from '@app/queries/servers/channel'; +import {queryPreferencesByCategory} from '@app/queries/servers/preference'; +import {getDirectChannelName} from '@app/utils/channel'; import {General, Preferences} from '@constants'; import {WithDatabaseArgs} from '@typings/database/database'; @@ -22,7 +25,7 @@ type ChannelData = Pick & { isMuted: boolean; }; -const {SERVER: {MY_CHANNEL_SETTINGS, PREFERENCE}} = MM_TABLES; +const {SERVER: {MY_CHANNEL_SETTINGS}} = MM_TABLES; const sortAlpha = (locale: string, a: ChannelData, b: ChannelData) => { if (a.isMuted && !b.isMuted) { @@ -82,19 +85,37 @@ const getSortedIds = (database: Database, category: CategoryModel, locale: strin } }; -const enhance = withObservables(['category'], ({category, locale, database}: {category: CategoryModel; locale: string} & WithDatabaseArgs) => { +const mapPrefName = (prefs: PreferenceModel[]) => of$(prefs.map((p) => p.name)); + +const mapChannelIds = (channels: ChannelModel[]) => of$(channels.map((c) => c.id)); + +const enhance = withObservables(['category'], ({category, locale, database, currentUserId}: {category: CategoryModel; locale: string; currentUserId: string} & WithDatabaseArgs) => { const observedCategory = category.observe(); const sortedIds = observedCategory.pipe( switchMap((c) => getSortedIds(database, c, locale)), ); + const dmMap = (p: PreferenceModel) => getDirectChannelName(p.name, currentUserId); + + const hiddenDmIds = queryPreferencesByCategory(database, Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, undefined, 'false'). + observe().pipe( + switchMap((prefs: PreferenceModel[]) => { + const names = prefs.map(dmMap); + const channels = queryChannelsByNames(database, names).observe(); + + return channels.pipe( + switchMap(mapChannelIds), + ); + }), + ); + + const hiddenGmIds = queryPreferencesByCategory(database, Preferences.CATEGORY_GROUP_CHANNEL_SHOW, undefined, 'false'). + observe().pipe(switchMap(mapPrefName)); + let limit = of$(0); if (category.type === 'direct_messages') { - limit = database.get(PREFERENCE). - query( - Q.where('category', Preferences.CATEGORY_SIDEBAR_SETTINGS), - Q.where('name', 'limit_visible_dms_gms'), - ).observe().pipe( + limit = queryPreferencesByCategory(database, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_LIMIT_DMS). + observe().pipe( switchMap( (val) => { if (val[0]) { @@ -107,8 +128,13 @@ const enhance = withObservables(['category'], ({category, locale, database}: {ca ); } + const hiddenChannelIds = combineLatest([hiddenDmIds, hiddenGmIds]).pipe(switchMap( + ([a, b]) => of$(a.concat(b)), + )); + return { limit, + hiddenChannelIds, sortedIds, category: observedCategory, }; diff --git a/app/components/channel_list/categories/categories.tsx b/app/components/channel_list/categories/categories.tsx index 406b12294..5718fd42b 100644 --- a/app/components/channel_list/categories/categories.tsx +++ b/app/components/channel_list/categories/categories.tsx @@ -14,6 +14,7 @@ import type {CategoryModel} from '@database/models/server'; type Props = { categories: CategoryModel[]; currentChannelId: string; + currentUserId: string; } const styles = StyleSheet.create({ @@ -24,8 +25,9 @@ const styles = StyleSheet.create({ const extractKey = (item: CategoryModel) => item.id; -const Categories = ({categories, currentChannelId}: Props) => { +const Categories = ({categories, currentChannelId, currentUserId}: Props) => { const intl = useIntl(); + const renderCategory = useCallback((data: {item: CategoryModel}) => { return ( <> @@ -33,6 +35,7 @@ const Categories = ({categories, currentChannelId}: Props) => { diff --git a/app/components/channel_list/categories/index.ts b/app/components/channel_list/categories/index.ts index cf6010965..7de7c61cb 100644 --- a/app/components/channel_list/categories/index.ts +++ b/app/components/channel_list/categories/index.ts @@ -16,7 +16,7 @@ import type CategoryModel from '@typings/database/models/servers/category'; import type SystemModel from '@typings/database/models/servers/system'; const {SERVER: {CATEGORY, SYSTEM}} = MM_TABLES; -const {CURRENT_CHANNEL_ID} = SYSTEM_IDENTIFIERS; +const {CURRENT_CHANNEL_ID, CURRENT_USER_ID} = SYSTEM_IDENTIFIERS; type WithDatabaseProps = {currentTeamId: string } & WithDatabaseArgs @@ -26,6 +26,9 @@ const enhanced = withObservables( const currentChannelId = database.get(SYSTEM).findAndObserve(CURRENT_CHANNEL_ID).pipe( switchMap(({value}) => of$(value)), ); + const currentUserId = database.get(SYSTEM).findAndObserve(CURRENT_USER_ID).pipe( + switchMap(({value}) => of$(value)), + ); const categories = database.get(CATEGORY).query( Q.where('team_id', currentTeamId), ).observeWithColumns(['sort_order']); @@ -33,6 +36,7 @@ const enhanced = withObservables( return { currentChannelId, categories, + currentUserId, }; }); diff --git a/app/constants/preferences.ts b/app/constants/preferences.ts index 0a962329a..c76a1d17b 100644 --- a/app/constants/preferences.ts +++ b/app/constants/preferences.ts @@ -11,6 +11,7 @@ const Preferences: Record = { CATEGORY_FAVORITE_CHANNEL: 'favorite_channel', CATEGORY_AUTO_RESET_MANUAL_STATUS: 'auto_reset_manual_status', CATEGORY_NOTIFICATIONS: 'notifications', + COMMENTS: 'comments', COMMENTS_ANY: 'any', COMMENTS_ROOT: 'root', @@ -35,7 +36,7 @@ const Preferences: Record = { USE_MILITARY_TIME: 'use_military_time', CATEGORY_SIDEBAR_SETTINGS: 'sidebar_settings', CHANNEL_SIDEBAR_ORGANIZATION: 'channel_sidebar_organization', - CHANNEL_SIDEBAR_AUTOCLOSE_DMS: 'close_unused_direct_messages', + CHANNEL_SIDEBAR_LIMIT_DMS: 'limit_visible_dms_gms', AUTOCLOSE_DMS_ENABLED: 'after_seven_days', CATEGORY_ADVANCED_SETTINGS: 'advanced_settings', ADVANCED_FILTER_JOIN_LEAVE: 'join_leave', diff --git a/app/queries/servers/channel.ts b/app/queries/servers/channel.ts index 48a9c0308..2683db156 100644 --- a/app/queries/servers/channel.ts +++ b/app/queries/servers/channel.ts @@ -247,3 +247,7 @@ export const addChannelMembership = async (operator: ServerDataOperator, userId: export const queryAllMyChannelMembershipIds = async (database: Database, userId: string) => { return database.get(CHANNEL_MEMBERSHIP).query(Q.where('user_id', Q.eq(userId))).fetchIds(); }; + +export const queryChannelsByNames = (database: Database, names: string[]) => { + return database.get(CHANNEL).query(Q.where('name', Q.oneOf(names))); +}; diff --git a/app/queries/servers/preference.ts b/app/queries/servers/preference.ts index 3290f04dc..d4fd7acc0 100644 --- a/app/queries/servers/preference.ts +++ b/app/queries/servers/preference.ts @@ -12,6 +12,8 @@ import {queryCurrentTeamId} from './system'; import type ServerDataOperator from '@database/operator/server_data_operator'; import type PreferenceModel from '@typings/database/models/servers/preference'; +const {SERVER: {PREFERENCE}} = MM_TABLES; + export const prepareMyPreferences = (operator: ServerDataOperator, preferences: PreferenceType[], sync = false) => { try { return operator.handlePreferences({ @@ -65,3 +67,17 @@ export const deletePreferences = async (database: ServerDatabase, preferences: P return false; } }; + +export const queryPreferencesByCategory = (database: Database, category: string, name?: string, value?: string) => { + const clauses = [Q.where('category', category)]; + + if (typeof name === 'string') { + clauses.push(Q.where('name', name)); + } + + if (typeof value === 'string') { + clauses.push(Q.where('value', value)); + } + + return database.get(PREFERENCE).query(...clauses); +}; From 7431fd41202d823fc3c2c3a033804398dce6cde6 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Fri, 18 Mar 2022 13:30:28 -0300 Subject: [PATCH 02/13] combine reactions by their first alias --- app/actions/local/reactions.ts | 12 ++-- app/actions/remote/reactions.ts | 68 +++++++++++-------- .../post/body/reactions/reactions.tsx | 19 ++++-- app/queries/servers/reaction.ts | 15 ++++ app/utils/emoji/helpers.ts | 4 ++ 5 files changed, 78 insertions(+), 40 deletions(-) create mode 100644 app/queries/servers/reaction.ts diff --git a/app/actions/local/reactions.ts b/app/actions/local/reactions.ts index 846db4cdb..3303c712e 100644 --- a/app/actions/local/reactions.ts +++ b/app/actions/local/reactions.ts @@ -3,6 +3,7 @@ import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database'; import DatabaseManager from '@database/manager'; +import {getEmojiFirstAlias} from '@utils/emoji/helpers'; import {safeParseJSON} from '@utils/helpers'; import type SystemModel from '@typings/database/models/servers/system'; @@ -29,16 +30,17 @@ export const addRecentReaction = async (serverUrl: string, emojiNames: string[], try { const recentEmojis = new Set(recent); - for (const name of emojiNames) { - if (recentEmojis.has(name)) { - recentEmojis.delete(name); + const aliases = emojiNames.map((e) => getEmojiFirstAlias(e)); + for (const alias of aliases) { + if (recentEmojis.has(alias)) { + recentEmojis.delete(alias); } } recent = Array.from(recentEmojis); - for (const name of emojiNames) { - recent.unshift(name); + for (const alias of aliases) { + recent.unshift(alias); } return operator.handleSystem({ systems: [{ diff --git a/app/actions/remote/reactions.ts b/app/actions/remote/reactions.ts index 3444df6ab..360333be0 100644 --- a/app/actions/remote/reactions.ts +++ b/app/actions/remote/reactions.ts @@ -1,14 +1,15 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Model, Q} from '@nozbe/watermelondb'; +import {Model} from '@nozbe/watermelondb'; import {addRecentReaction} from '@actions/local/reactions'; -import {MM_TABLES} from '@constants/database'; import DatabaseManager from '@database/manager'; import NetworkManager from '@init/network_manager'; import {queryRecentPostsInChannel, queryRecentPostsInThread} from '@queries/servers/post'; +import {queryReaction} from '@queries/servers/reaction'; import {queryCurrentChannelId, queryCurrentUserId} from '@queries/servers/system'; +import {getEmojiFirstAlias} from '@utils/emoji/helpers'; import {forceLogoutIfNecessary} from './session'; @@ -30,29 +31,41 @@ export const addReaction = async (serverUrl: string, postId: string, emojiName: try { const currentUserId = await queryCurrentUserId(operator.database); - const reaction = await client.addReaction(currentUserId, postId, emojiName); - const models: Model[] = []; + const emojiAlias = getEmojiFirstAlias(emojiName); + const reacted = await queryReaction(operator.database, emojiAlias, postId, currentUserId).fetchCount() > 0; + if (!reacted) { + const reaction = await client.addReaction(currentUserId, postId, emojiAlias); + const models: Model[] = []; - const reactions = await operator.handleReactions({ - postsReactions: [{ + const reactions = await operator.handleReactions({ + postsReactions: [{ + post_id: postId, + reactions: [reaction], + }], + prepareRecordsOnly: true, + skipSync: true, // this prevents the handler from deleting previous reactions + }); + models.push(...reactions); + + const recent = await addRecentReaction(serverUrl, [emojiName], true); + if (Array.isArray(recent)) { + models.push(...recent); + } + + if (models.length) { + await operator.batchRecords(models); + } + + return {reaction}; + } + return { + reaction: { + user_id: currentUserId, post_id: postId, - reactions: [reaction], - }], - prepareRecordsOnly: true, - skipSync: true, // this prevents the handler from deleting previous reactions - }); - models.push(...reactions); - - const recent = await addRecentReaction(serverUrl, [emojiName], true); - if (Array.isArray(recent)) { - models.push(...recent); - } - - if (models.length) { - await operator.batchRecords(models); - } - - return {reaction}; + emoji_name: emojiAlias, + create_at: Date.now(), + } as Reaction, + }; } catch (error) { forceLogoutIfNecessary(serverUrl, error as ClientErrorProps); return {error}; @@ -74,14 +87,11 @@ export const removeReaction = async (serverUrl: string, postId: string, emojiNam try { const currentUserId = await queryCurrentUserId(database); - await client.removeReaction(currentUserId, postId, emojiName); + const emojiAlias = getEmojiFirstAlias(emojiName); + await client.removeReaction(currentUserId, postId, emojiAlias); // should return one or no reaction - const reaction = await database.get(MM_TABLES.SERVER.REACTION).query( - Q.where('emoji_name', emojiName), - Q.where('post_id', postId), - Q.where('user_id', currentUserId), - ).fetch(); + const reaction = await queryReaction(database, emojiAlias, postId, currentUserId).fetch(); if (reaction.length) { await database.write(async () => { diff --git a/app/components/post_list/post/body/reactions/reactions.tsx b/app/components/post_list/post/body/reactions/reactions.tsx index f12619100..8baa12c84 100644 --- a/app/components/post_list/post/body/reactions/reactions.tsx +++ b/app/components/post_list/post/body/reactions/reactions.tsx @@ -10,6 +10,7 @@ import CompassIcon from '@components/compass_icon'; import {MAX_ALLOWED_REACTIONS} from '@constants/emoji'; import {useServerUrl} from '@context/server'; import {showModal, showModalOverCurrentContext} from '@screens/navigation'; +import {getEmojiFirstAlias} from '@utils/emoji/helpers'; import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; @@ -59,12 +60,12 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled, const intl = useIntl(); const serverUrl = useServerUrl(); const pressed = useRef(false); - const [sortedReactions, setSortedReactions] = useState(new Set(reactions.map((r) => r.emojiName))); + const [sortedReactions, setSortedReactions] = useState(new Set(reactions.map((r) => getEmojiFirstAlias(r.emojiName)))); const styles = getStyleSheet(theme); useEffect(() => { // This helps keep the reactions in the same position at all times until unmounted - const rs = reactions.map((r) => r.emojiName); + const rs = reactions.map((r) => getEmojiFirstAlias(r.emojiName)); const sorted = new Set([...sortedReactions]); const added = rs.filter((r) => !sorted.has(r)); added.forEach(sorted.add, sorted); @@ -78,14 +79,20 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled, const reactionsByName = reactions.reduce((acc, reaction) => { if (reaction) { - if (acc.has(reaction.emojiName)) { - acc.get(reaction.emojiName)!.push(reaction); + const emojiAlias = getEmojiFirstAlias(reaction.emojiName); + if (acc.has(emojiAlias)) { + const rs = acc.get(emojiAlias); + // eslint-disable-next-line max-nested-callbacks + const present = rs!.findIndex((r) => r.userId === reaction.userId) > -1; + if (!present) { + rs!.push(reaction); + } } else { - acc.set(reaction.emojiName, [reaction]); + acc.set(emojiAlias, [reaction]); } if (reaction.userId === currentUserId) { - highlightedReactions.push(reaction.emojiName); + highlightedReactions.push(emojiAlias); } } diff --git a/app/queries/servers/reaction.ts b/app/queries/servers/reaction.ts new file mode 100644 index 000000000..8b7176618 --- /dev/null +++ b/app/queries/servers/reaction.ts @@ -0,0 +1,15 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Database, Q} from '@nozbe/watermelondb'; + +import {MM_TABLES} from '@constants/database'; +const {SERVER: {REACTION}} = MM_TABLES; + +export const queryReaction = (database: Database, emojiName: string, postId: string, userId: string) => { + return database.get(REACTION).query( + Q.where('emoji_name', emojiName), + Q.where('post_id', postId), + Q.where('user_id', userId), + ); +}; diff --git a/app/utils/emoji/helpers.ts b/app/utils/emoji/helpers.ts index 0fb435f9b..f3ea5b334 100644 --- a/app/utils/emoji/helpers.ts +++ b/app/utils/emoji/helpers.ts @@ -193,6 +193,10 @@ export function doesMatchNamedEmoji(emojiName: string) { return false; } +export const getEmojiFirstAlias = (emoji: string) => { + return getEmojiByName(emoji, [])?.short_names?.[0] || emoji; +}; + export function getEmojiByName(emojiName: string, customEmojis: CustomEmojiModel[]) { if (EmojiIndicesByAlias.has(emojiName)) { return Emojis[EmojiIndicesByAlias.get(emojiName)!]; From 1448ee843a89e3c283910ba140e651aa4c6d7b8d Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Tue, 22 Mar 2022 14:08:49 -0300 Subject: [PATCH 03/13] Extract re-usable user_item component --- .../at_mention_item/at_mention_item.tsx | 175 ------------------ .../autocomplete/at_mention_item/index.tsx | 49 +++++ .../at_mention_item => user_item}/index.ts | 4 +- app/components/user_item/user_item.tsx | 174 +++++++++++++++++ app/utils/user/index.ts | 14 +- 5 files changed, 234 insertions(+), 182 deletions(-) delete mode 100644 app/components/autocomplete/at_mention_item/at_mention_item.tsx create mode 100644 app/components/autocomplete/at_mention_item/index.tsx rename app/components/{autocomplete/at_mention_item => user_item}/index.ts (92%) create mode 100644 app/components/user_item/user_item.tsx diff --git a/app/components/autocomplete/at_mention_item/at_mention_item.tsx b/app/components/autocomplete/at_mention_item/at_mention_item.tsx deleted file mode 100644 index b1227e778..000000000 --- a/app/components/autocomplete/at_mention_item/at_mention_item.tsx +++ /dev/null @@ -1,175 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {useCallback} from 'react'; -import {Text, View} from 'react-native'; -import {useSafeAreaInsets} from 'react-native-safe-area-context'; - -import ChannelIcon from '@components/channel_icon'; -import CustomStatusEmoji from '@components/custom_status/custom_status_emoji'; -import FormattedText from '@components/formatted_text'; -import ProfilePicture from '@components/profile_picture'; -import {BotTag, GuestTag} from '@components/tag'; -import TouchableWithFeedback from '@components/touchable_with_feedback'; -import {General} from '@constants'; -import {useTheme} from '@context/theme'; -import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme'; -import {getUserCustomStatus, isGuest, isShared} from '@utils/user'; - -type AtMentionItemProps = { - user: UserProfile; - currentUserId: string; - onPress: (username: string) => void; - showFullName: boolean; - testID?: string; - isCustomStatusEnabled: boolean; -} - -const getName = (user: UserProfile, showFullName: boolean, isCurrentUser: boolean) => { - let name = ''; - const hasNickname = user.nickname.length > 0; - - if (showFullName) { - name += `${user.first_name} ${user.last_name} `; - } - - if (hasNickname && !isCurrentUser) { - name += name.length > 0 ? `(${user.nickname})` : user.nickname; - } - - return name.trim(); -}; - -const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { - return { - row: { - height: 40, - paddingVertical: 8, - paddingTop: 4, - paddingHorizontal: 16, - flexDirection: 'row', - alignItems: 'center', - }, - rowPicture: { - marginRight: 10, - marginLeft: 2, - width: 24, - alignItems: 'center', - justifyContent: 'center', - }, - rowInfo: { - flexDirection: 'row', - overflow: 'hidden', - }, - rowFullname: { - fontSize: 15, - color: theme.centerChannelColor, - paddingLeft: 4, - flexShrink: 1, - }, - rowUsername: { - color: changeOpacity(theme.centerChannelColor, 0.56), - fontSize: 15, - flexShrink: 5, - }, - icon: { - marginLeft: 4, - }, - }; -}); - -const AtMentionItem = ({ - user, - currentUserId, - onPress, - showFullName, - testID, - isCustomStatusEnabled, -}: AtMentionItemProps) => { - const insets = useSafeAreaInsets(); - const theme = useTheme(); - const style = getStyleFromTheme(theme); - - const guest = isGuest(user.roles); - const shared = isShared(user); - - const completeMention = useCallback(() => { - onPress(user.username); - }, [user.username]); - - const isCurrentUser = currentUserId === user.id; - const name = getName(user, showFullName, isCurrentUser); - const customStatus = getUserCustomStatus(user); - - return ( - - - - - - - {Boolean(user.is_bot) && ()} - {guest && ()} - {Boolean(name.length) && ( - - {name} - - )} - {isCurrentUser && ( - - )} - - {` @${user.username}`} - - - {isCustomStatusEnabled && !user.is_bot && customStatus && ( - - )} - {shared && ( - - )} - - - ); -}; - -export default AtMentionItem; diff --git a/app/components/autocomplete/at_mention_item/index.tsx b/app/components/autocomplete/at_mention_item/index.tsx new file mode 100644 index 000000000..ecc062a5f --- /dev/null +++ b/app/components/autocomplete/at_mention_item/index.tsx @@ -0,0 +1,49 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback} from 'react'; +import {useSafeAreaInsets} from 'react-native-safe-area-context'; + +import TouchableWithFeedback from '@components/touchable_with_feedback'; +import UserItem from '@components/user_item'; +import {useTheme} from '@context/theme'; +import {changeOpacity} from '@utils/theme'; + +import type UserModel from '@typings/database/models/servers/user'; + +type AtMentionItemProps = { + user: UserProfile | UserModel; + onPress?: (username: string) => void; + testID?: string; +} + +const AtMentionItem = ({ + user, + onPress, + testID, +}: AtMentionItemProps) => { + const insets = useSafeAreaInsets(); + const theme = useTheme(); + + const completeMention = useCallback(() => { + onPress?.(user.username); + }, [user.username]); + + return ( + + + + ); +}; + +export default AtMentionItem; diff --git a/app/components/autocomplete/at_mention_item/index.ts b/app/components/user_item/index.ts similarity index 92% rename from app/components/autocomplete/at_mention_item/index.ts rename to app/components/user_item/index.ts index eed265068..af20dbdce 100644 --- a/app/components/autocomplete/at_mention_item/index.ts +++ b/app/components/user_item/index.ts @@ -8,7 +8,7 @@ import {switchMap} from 'rxjs/operators'; import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database'; -import AtMentionItem from './at_mention_item'; +import UserItem from './user_item'; import type {WithDatabaseArgs} from '@typings/database/database'; import type SystemModel from '@typings/database/models/servers/system'; @@ -34,4 +34,4 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => { }; }); -export default withDatabase(enhanced(AtMentionItem)); +export default withDatabase(enhanced(UserItem)); diff --git a/app/components/user_item/user_item.tsx b/app/components/user_item/user_item.tsx new file mode 100644 index 000000000..a6e91217f --- /dev/null +++ b/app/components/user_item/user_item.tsx @@ -0,0 +1,174 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {IntlShape, useIntl} from 'react-intl'; +import {StyleProp, Text, View, ViewStyle} from 'react-native'; + +import ChannelIcon from '@components/channel_icon'; +import CustomStatusEmoji from '@components/custom_status/custom_status_emoji'; +import FormattedText from '@components/formatted_text'; +import ProfilePicture from '@components/profile_picture'; +import {BotTag, GuestTag} from '@components/tag'; +import {General} from '@constants'; +import {useTheme} from '@context/theme'; +import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme'; +import {getUserCustomStatus, isBot, isGuest, isShared} from '@utils/user'; + +import type UserModel from '@typings/database/models/servers/user'; + +type AtMentionItemProps = { + user?: UserProfile | UserModel; + containerStyle?: StyleProp; + currentUserId: string; + showFullName: boolean; + testID?: string; + isCustomStatusEnabled: boolean; +} + +const getName = (user: UserProfile | UserModel | undefined, showFullName: boolean, isCurrentUser: boolean, intl: IntlShape) => { + let name = ''; + if (!user) { + return intl.formatMessage({id: 'channel_loader.someone', defaultMessage: 'Someone'}); + } + + const hasNickname = user.nickname.length > 0; + + if (showFullName) { + const first = 'first_name' in user ? user.first_name : user.firstName; + const last = 'last_name' in user ? user.last_name : user.lastName; + name += `${first} ${last} `; + } + + if (hasNickname && !isCurrentUser) { + name += name.length > 0 ? `(${user.nickname})` : user.nickname; + } + + return name.trim(); +}; + +const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { + return { + row: { + height: 40, + paddingVertical: 8, + paddingTop: 4, + paddingHorizontal: 16, + flexDirection: 'row', + alignItems: 'center', + }, + rowPicture: { + marginRight: 10, + marginLeft: 2, + width: 24, + alignItems: 'center', + justifyContent: 'center', + }, + rowInfo: { + flexDirection: 'row', + overflow: 'hidden', + }, + rowFullname: { + fontSize: 15, + color: theme.centerChannelColor, + fontFamily: 'OpenSans', + paddingLeft: 4, + flexShrink: 1, + }, + rowUsername: { + color: changeOpacity(theme.centerChannelColor, 0.56), + fontSize: 15, + fontFamily: 'OpenSans', + flexShrink: 5, + }, + icon: { + marginLeft: 4, + }, + }; +}); + +const UserItem = ({ + containerStyle, + user, + currentUserId, + showFullName, + testID, + isCustomStatusEnabled, +}: AtMentionItemProps) => { + const theme = useTheme(); + const style = getStyleFromTheme(theme); + const intl = useIntl(); + + const bot = user ? isBot(user) : false; + const guest = user ? isGuest(user.roles) : false; + const shared = user ? isShared(user) : false; + + const isCurrentUser = currentUserId === user?.id; + const name = getName(user, showFullName, isCurrentUser, intl); + const customStatus = getUserCustomStatus(user); + + return ( + + + + + + {bot && } + {guest && } + {Boolean(name.length) && + + {name} + + } + {isCurrentUser && + + } + {Boolean(user) && + + {` @${user!.username}`} + + } + + {isCustomStatusEnabled && !bot && customStatus && ( + + )} + {shared && ( + + )} + + ); +}; + +export default UserItem; diff --git a/app/utils/user/index.ts b/app/utils/user/index.ts index 147e62744..e0e60b02e 100644 --- a/app/utils/user/index.ts +++ b/app/utils/user/index.ts @@ -123,13 +123,13 @@ export const getTimezone = (timezone: UserTimezone | null) => { return timezone.manualTimezone; }; -export const getUserCustomStatus = (user: UserModel | UserProfile): UserCustomStatus | undefined => { +export const getUserCustomStatus = (user?: UserModel | UserProfile): UserCustomStatus | undefined => { try { - if (typeof user.props?.customStatus === 'string') { + if (typeof user?.props?.customStatus === 'string') { return JSON.parse(user.props.customStatus) as UserCustomStatus; } - return user.props?.customStatus; + return user?.props?.customStatus; } catch { return undefined; } @@ -192,8 +192,12 @@ export function confirmOutOfOfficeDisabled(intl: IntlShape, status: string, upda ); } -export function isShared(user: UserProfile): boolean { - return Boolean(user.remote_id); +export function isBot(user: UserProfile | UserModel): boolean { + return 'is_bot' in user ? Boolean(user.is_bot) : Boolean(user.isBot); +} + +export function isShared(user: UserProfile | UserModel): boolean { + return 'remote_id' in user ? Boolean(user.remote_id) : Boolean(user.props?.remote_id); } export function removeUserFromList(userId: string, originalList: UserProfile[]): UserProfile[] { From a0ff99f4e974ff350777820ddc51de7e837de670 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Tue, 22 Mar 2022 14:11:48 -0300 Subject: [PATCH 04/13] set maxWidth of reaction based on the amount of digits --- .../post/body/reactions/reaction.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app/components/post_list/post/body/reactions/reaction.tsx b/app/components/post_list/post/body/reactions/reaction.tsx index 1be845a4d..087c17cef 100644 --- a/app/components/post_list/post/body/reactions/reaction.tsx +++ b/app/components/post_list/post/body/reactions/reaction.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useCallback} from 'react'; +import React, {useCallback, useMemo} from 'react'; import {TouchableOpacity, View} from 'react-native'; import AnimatedNumbers from 'react-native-animated-numbers'; @@ -14,7 +14,7 @@ type ReactionProps = { emojiName: string; highlight: boolean; onPress: (emojiName: string, highlight: boolean) => void; - onLongPress: () => void; + onLongPress: (initialEmoji: string) => void; theme: Theme; } @@ -24,7 +24,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { color: changeOpacity(theme.centerChannelColor, 0.56), ...typography('Body', 100, 'SemiBold'), }, - countContainer: {marginRight: 5}, + countContainer: {marginRight: 8}, countHighlight: { color: theme.buttonBg, }, @@ -51,6 +51,15 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { const Reaction = ({count, emojiName, highlight, onPress, onLongPress, theme}: ReactionProps) => { const styles = getStyleSheet(theme); + const digits = String(count).length; + const containerStyle = useMemo(() => { + const minWidth = 50 + (digits * 5); + return [styles.reaction, (highlight && styles.highlight), {minWidth}]; + }, [styles.reaction, highlight, digits]); + + const handleLongPress = useCallback(() => { + onLongPress(emojiName); + }, []); const handlePress = useCallback(() => { onPress(emojiName, highlight); @@ -59,9 +68,9 @@ const Reaction = ({count, emojiName, highlight, onPress, onLongPress, theme}: Re return ( Date: Tue, 22 Mar 2022 14:30:44 -0300 Subject: [PATCH 05/13] constants --- app/constants/screens.ts | 23 ++++++++++++++++------- app/utils/theme/index.ts | 17 +---------------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/app/constants/screens.ts b/app/constants/screens.ts index db43ef32d..22dd7d37a 100644 --- a/app/constants/screens.ts +++ b/app/constants/screens.ts @@ -3,7 +3,7 @@ export const ABOUT = 'About'; export const ACCOUNT = 'Account'; -export const EMOJI_PICKER = 'AddReaction'; +export const EMOJI_PICKER = 'EmojiPicker'; export const APP_FORM = 'AppForm'; export const BOTTOM_SHEET = 'BottomSheet'; export const BROWSE_CHANNELS = 'BrowseChannels'; @@ -11,6 +11,7 @@ export const CHANNEL = 'Channel'; export const CHANNEL_ADD_PEOPLE = 'ChannelAddPeople'; export const CHANNEL_DETAILS = 'ChannelDetails'; export const CHANNEL_EDIT = 'ChannelEdit'; +export const CREATE_DIRECT_MESSAGE = 'CreateDirectMessage'; export const CUSTOM_STATUS_CLEAR_AFTER = 'CustomStatusClearAfter'; export const CUSTOM_STATUS = 'CustomStatus'; export const EDIT_POST = 'EditPost'; @@ -24,16 +25,16 @@ export const IN_APP_NOTIFICATION = 'InAppNotification'; export const LOGIN = 'Login'; export const MENTIONS = 'Mentions'; export const MFA = 'MFA'; -export const CREATE_DIRECT_MESSAGE = 'CreateDirectMessage'; export const PERMALINK = 'Permalink'; +export const POST_OPTIONS = 'PostOptions'; +export const REACTIONS = 'Reactions'; +export const SAVED_POSTS = 'SavedPosts'; export const SEARCH = 'Search'; export const SERVER = 'Server'; export const SETTINGS_SIDEBAR = 'SettingsSidebar'; export const SSO = 'SSO'; export const THREAD = 'Thread'; export const USER_PROFILE = 'UserProfile'; -export const POST_OPTIONS = 'PostOptions'; -export const SAVED_POSTS = 'SavedPosts'; export default { ABOUT, @@ -46,6 +47,7 @@ export default { CHANNEL_ADD_PEOPLE, CHANNEL_EDIT, CHANNEL_DETAILS, + CREATE_DIRECT_MESSAGE, CUSTOM_STATUS_CLEAR_AFTER, CUSTOM_STATUS, EDIT_POST, @@ -59,14 +61,21 @@ export default { LOGIN, MENTIONS, MFA, - CREATE_DIRECT_MESSAGE, PERMALINK, + POST_OPTIONS, + REACTIONS, + SAVED_POSTS, SEARCH, SERVER, SETTINGS_SIDEBAR, SSO, THREAD, USER_PROFILE, - POST_OPTIONS, - SAVED_POSTS, }; + +export const MODAL_SCREENS_WITHOUT_BACK = [ + CREATE_DIRECT_MESSAGE, + EMOJI_PICKER, + EDIT_POST, + PERMALINK, +]; diff --git a/app/utils/theme/index.ts b/app/utils/theme/index.ts index 4bdfdadc5..1af040555 100644 --- a/app/utils/theme/index.ts +++ b/app/utils/theme/index.ts @@ -6,27 +6,12 @@ import {StatusBar, StyleSheet} from 'react-native'; import tinyColor from 'tinycolor2'; import {Preferences} from '@constants'; +import {MODAL_SCREENS_WITHOUT_BACK} from '@constants/screens'; import {appearanceControlledScreens, mergeNavigationOptions} from '@screens/navigation'; import EphemeralStore from '@store/ephemeral_store'; import type {Options} from 'react-native-navigation'; -const MODAL_SCREENS_WITHOUT_BACK = [ - 'AddReaction', - 'ChannelInfo', - 'ClientUpgrade', - 'CreateChannel', - 'EditPost', - 'ErrorTeamsList', - 'MoreChannels', - 'MoreDirectMessages', - 'Permalink', - 'SelectTeam', - 'Settings', - 'TermsOfService', - 'UserProfile', -]; - const rgbPattern = /^rgba?\((\d+),(\d+),(\d+)(?:,([\d.]+))?\)$/; export function getComponents(inColor: string): {red: number; green: number; blue: number; alpha: number} { From 0618a10e1c86c9bc9cd8cd5e652757ec9aa3ea72 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Tue, 22 Mar 2022 14:32:57 -0300 Subject: [PATCH 06/13] Reactions screen --- .../post/body/reactions/reactions.tsx | 36 +++++--- app/screens/index.tsx | 6 ++ app/screens/reactions/emoji_aliases/index.tsx | 44 +++++++++ app/screens/reactions/emoji_bar/index.tsx | 91 +++++++++++++++++++ app/screens/reactions/emoji_bar/item.tsx | 81 +++++++++++++++++ app/screens/reactions/index.ts | 32 +++++++ app/screens/reactions/reactions.tsx | 84 +++++++++++++++++ app/screens/reactions/reactors_list/index.tsx | 70 ++++++++++++++ .../reactions/reactors_list/reactor/index.ts | 33 +++++++ .../reactors_list/reactor/reactor.tsx | 33 +++++++ 10 files changed, 497 insertions(+), 13 deletions(-) create mode 100644 app/screens/reactions/emoji_aliases/index.tsx create mode 100644 app/screens/reactions/emoji_bar/index.tsx create mode 100644 app/screens/reactions/emoji_bar/item.tsx create mode 100644 app/screens/reactions/index.ts create mode 100644 app/screens/reactions/reactions.tsx create mode 100644 app/screens/reactions/reactors_list/index.tsx create mode 100644 app/screens/reactions/reactors_list/reactor/index.ts create mode 100644 app/screens/reactions/reactors_list/reactor/reactor.tsx diff --git a/app/components/post_list/post/body/reactions/reactions.tsx b/app/components/post_list/post/body/reactions/reactions.tsx index 8baa12c84..ff63db352 100644 --- a/app/components/post_list/post/body/reactions/reactions.tsx +++ b/app/components/post_list/post/body/reactions/reactions.tsx @@ -3,13 +3,15 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; import {useIntl} from 'react-intl'; -import {TouchableOpacity, View} from 'react-native'; +import {Keyboard, TouchableOpacity, View} from 'react-native'; import {addReaction, removeReaction} from '@actions/remote/reactions'; import CompassIcon from '@components/compass_icon'; +import {Screens} from '@constants'; import {MAX_ALLOWED_REACTIONS} from '@constants/emoji'; import {useServerUrl} from '@context/server'; -import {showModal, showModalOverCurrentContext} from '@screens/navigation'; +import {useIsTablet} from '@hooks/device'; +import {bottomSheetModalOptions, showModal, showModalOverCurrentContext} from '@screens/navigation'; import {getEmojiFirstAlias} from '@utils/emoji/helpers'; import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; @@ -59,6 +61,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled, postId, reactions, theme}: ReactionsProps) => { const intl = useIntl(); const serverUrl = useServerUrl(); + const isTablet = useIsTablet(); const pressed = useRef(false); const [sortedReactions, setSortedReactions] = useState(new Set(reactions.map((r) => getEmojiFirstAlias(r.emojiName)))); const styles = getStyleSheet(theme); @@ -106,8 +109,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled, addReaction(serverUrl, postId, emoji); }; - const handleAddReaction = preventDoubleTap(() => { - const screen = 'AddReaction'; + const handleAddReaction = useCallback(preventDoubleTap(() => { const title = intl.formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}); const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor); @@ -116,10 +118,10 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled, onEmojiPress: handleAddReactionToPost, }; - showModal(screen, title, passProps); - }); + showModal(Screens.EMOJI_PICKER, title, passProps); + }), [intl, theme]); - const handleReactionPress = async (emoji: string, remove: boolean) => { + const handleReactionPress = useCallback(async (emoji: string, remove: boolean) => { pressed.current = true; if (remove && canRemoveReaction && !disabled) { await removeReaction(serverUrl, postId, emoji); @@ -128,18 +130,26 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled, } pressed.current = false; - }; + }, [canRemoveReaction, canAddReaction, disabled]); - const showReactionList = () => { - const screen = 'ReactionList'; + const showReactionList = useCallback((initialEmoji: string) => { + const screen = Screens.REACTIONS; const passProps = { + initialEmoji, postId, }; + Keyboard.dismiss(); + const title = isTablet ? intl.formatMessage({id: 'post.reactions.title', defaultMessage: 'Reactions'}) : ''; + if (!pressed.current) { - showModalOverCurrentContext(screen, passProps); + if (isTablet) { + showModal(screen, title, passProps, bottomSheetModalOptions(theme, 'close-post-reactions')); + } else { + showModalOverCurrentContext(screen, passProps); + } } - }; + }, [intl, postId, theme]); let addMoreReactions = null; const {reactionsByName, highlightedReactions} = buildReactionsMap(); @@ -182,4 +192,4 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled, ); }; -export default React.memo(Reactions); +export default Reactions; diff --git a/app/screens/index.tsx b/app/screens/index.tsx index ab684bccb..bfbb67bae 100644 --- a/app/screens/index.tsx +++ b/app/screens/index.tsx @@ -80,6 +80,9 @@ Navigation.setLazyComponentRegistrator((screenName) => { require('@screens/custom_status_clear_after').default, ); break; + case Screens.CREATE_DIRECT_MESSAGE: + screen = withServerDatabase(require('@screens/create_direct_message').default); + break; case Screens.EDIT_POST: screen = withServerDatabase(require('@screens/edit_post').default); break; @@ -127,6 +130,9 @@ Navigation.setLazyComponentRegistrator((screenName) => { require('@screens/post_options').default, ); break; + case Screens.REACTIONS: + screen = withServerDatabase(require('@screens/reactions').default); + break; case Screens.SAVED_POSTS: screen = withServerDatabase((require('@screens/home/saved_posts').default)); break; diff --git a/app/screens/reactions/emoji_aliases/index.tsx b/app/screens/reactions/emoji_aliases/index.tsx new file mode 100644 index 000000000..5e5cb6f25 --- /dev/null +++ b/app/screens/reactions/emoji_aliases/index.tsx @@ -0,0 +1,44 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {Text, View} from 'react-native'; + +import {useTheme} from '@context/theme'; +import {getEmojiByName} from '@utils/emoji/helpers'; +import {makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; + +type Props = { + emoji: string; +}; + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ + container: { + marginBottom: 16, + }, + title: { + color: theme.centerChannelColor, + ...typography('Body', 75, 'SemiBold'), + }, +})); + +const EmojiAliases = ({emoji}: Props) => { + const theme = useTheme(); + const style = getStyleSheet(theme); + const aliases = getEmojiByName(emoji, [])?.short_names?.map((n: string) => `:${n}:`).join(' ') || `:${emoji}:`; + + return ( + + + {aliases} + + + ); +}; + +export default EmojiAliases; diff --git a/app/screens/reactions/emoji_bar/index.tsx b/app/screens/reactions/emoji_bar/index.tsx new file mode 100644 index 000000000..092dbbad0 --- /dev/null +++ b/app/screens/reactions/emoji_bar/index.tsx @@ -0,0 +1,91 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback, useEffect, useRef} from 'react'; +import {StyleSheet} from 'react-native'; +import {FlatList} from 'react-native-gesture-handler'; + +import Item from './item'; + +import type ReactionModel from '@typings/database/models/servers/reaction'; + +type Props = { + emojiSelected: string; + reactionsByName: Map; + setIndex: (idx: number) => void; + sortedReactions: string[]; +} + +type ScrollIndexFailed = { + index: number; + highestMeasuredFrameIndex: number; + averageItemLength: number; +}; + +const style = StyleSheet.create({ + container: { + maxHeight: 44, + }, +}); + +const EmojiBar = ({emojiSelected, reactionsByName, setIndex, sortedReactions}: Props) => { + const listRef = useRef>(null); + + const scrollToIndex = (index: number, animated = false) => { + listRef.current?.scrollToIndex({ + animated, + index, + viewOffset: 0, + viewPosition: 1, // 0 is at bottom + }); + }; + + const onPress = useCallback((emoji: string) => { + const index = sortedReactions.indexOf(emoji); + setIndex(index); + }, [sortedReactions]); + + const onScrollToIndexFailed = useCallback((info: ScrollIndexFailed) => { + const index = Math.min(info.highestMeasuredFrameIndex, info.index); + + scrollToIndex(index); + }, []); + + const renderItem = useCallback(({item}) => { + return ( + + ); + }, [sortedReactions, emojiSelected, reactionsByName]); + + useEffect(() => { + const t = setTimeout(() => { + listRef.current?.scrollToItem({ + item: emojiSelected, + animated: false, + viewPosition: 1, + }); + }, 100); + + return () => clearTimeout(t); + }, []); + + return ( + + ); +}; + +export default EmojiBar; diff --git a/app/screens/reactions/emoji_bar/item.tsx b/app/screens/reactions/emoji_bar/item.tsx new file mode 100644 index 000000000..3464b4e3b --- /dev/null +++ b/app/screens/reactions/emoji_bar/item.tsx @@ -0,0 +1,81 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback} from 'react'; +import {TouchableOpacity, View} from 'react-native'; +import AnimatedNumbers from 'react-native-animated-numbers'; + +import Emoji from '@components/emoji'; +import {useTheme} from '@context/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; + +type ReactionProps = { + count: number; + emojiName: string; + highlight: boolean; + onPress: (emojiName: string) => void; +} + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { + return { + count: { + color: changeOpacity(theme.centerChannelColor, 0.56), + ...typography('Body', 100, 'SemiBold'), + }, + countContainer: {marginRight: 5}, + countHighlight: { + color: theme.buttonBg, + }, + customEmojiStyle: {color: '#000'}, + emoji: {marginHorizontal: 5}, + highlight: { + backgroundColor: changeOpacity(theme.buttonBg, 0.08), + }, + reaction: { + alignItems: 'center', + borderRadius: 4, + backgroundColor: theme.centerChannelBg, + flexDirection: 'row', + height: 32, + justifyContent: 'center', + marginRight: 12, + minWidth: 50, + }, + }; +}); + +const Reaction = ({count, emojiName, highlight, onPress}: ReactionProps) => { + const theme = useTheme(); + const styles = getStyleSheet(theme); + + const handlePress = useCallback(() => { + onPress(emojiName); + }, [highlight]); + + return ( + + + + + + + + + ); +}; + +export default Reaction; diff --git a/app/screens/reactions/index.ts b/app/screens/reactions/index.ts new file mode 100644 index 000000000..0d76d0cd2 --- /dev/null +++ b/app/screens/reactions/index.ts @@ -0,0 +1,32 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; +import withObservables from '@nozbe/with-observables'; +import {switchMap} from 'rxjs/operators'; + +import {MM_TABLES} from '@constants/database'; + +import Reactions from './reactions'; + +import type {WithDatabaseArgs} from '@typings/database/database'; +import type PostModel from '@typings/database/models/servers/post'; + +type EnhancedProps = WithDatabaseArgs & { + postId: string; +} + +const {POST} = MM_TABLES.SERVER; + +const enhanced = withObservables([], ({postId, database}: EnhancedProps) => { + const post = database.get(POST).findAndObserve(postId); + + return { + reactions: post.pipe( + switchMap((p) => p.reactions.observe()), + ), + }; +}); + +export default withDatabase(enhanced(Reactions)); + diff --git a/app/screens/reactions/reactions.tsx b/app/screens/reactions/reactions.tsx new file mode 100644 index 000000000..6208e1a1e --- /dev/null +++ b/app/screens/reactions/reactions.tsx @@ -0,0 +1,84 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback, useEffect, useMemo, useState} from 'react'; + +import {Screens} from '@constants'; +import BottomSheet from '@screens/bottom_sheet'; +import {getEmojiFirstAlias} from '@utils/emoji/helpers'; + +import EmojiAliases from './emoji_aliases'; +import EmojiBar from './emoji_bar'; +import ReactorsList from './reactors_list'; + +import type ReactionModel from '@typings/database/models/servers/reaction'; + +type Props = { + initialEmoji: string; + reactions: ReactionModel[]; +} + +const Reactions = ({initialEmoji, reactions}: Props) => { + const [sortedReactions, setSortedReactions] = useState(Array.from(new Set(reactions.map((r) => getEmojiFirstAlias(r.emojiName))))); + const [index, setIndex] = useState(sortedReactions.indexOf(initialEmoji)); + const reactionsByName = useMemo(() => { + return reactions.reduce((acc, reaction) => { + const emojiAlias = getEmojiFirstAlias(reaction.emojiName); + if (acc.has(emojiAlias)) { + const rs = acc.get(emojiAlias); + // eslint-disable-next-line max-nested-callbacks + const present = rs!.findIndex((r) => r.userId === reaction.userId) > -1; + if (!present) { + rs!.push(reaction); + } + } else { + acc.set(emojiAlias, [reaction]); + } + + return acc; + }, new Map()); + }, [reactions]); + + const renderContent = useCallback(() => { + const emojiAlias = sortedReactions[index]; + + return ( + <> + + + + + ); + }, [index, reactions, sortedReactions]); + + useEffect(() => { + // This helps keep the reactions in the same position at all times until unmounted + const rs = reactions.map((r) => getEmojiFirstAlias(r.emojiName)); + const sorted = new Set([...sortedReactions]); + const added = rs.filter((r) => !sorted.has(r)); + added.forEach(sorted.add, sorted); + const removed = [...sorted].filter((s) => !rs.includes(s)); + removed.forEach(sorted.delete, sorted); + setSortedReactions(Array.from(sorted)); + }, [reactions]); + + return ( + + ); +}; + +export default Reactions; diff --git a/app/screens/reactions/reactors_list/index.tsx b/app/screens/reactions/reactors_list/index.tsx new file mode 100644 index 000000000..032ae1e5d --- /dev/null +++ b/app/screens/reactions/reactors_list/index.tsx @@ -0,0 +1,70 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback, useEffect, useRef, useState} from 'react'; +import {NativeScrollEvent, NativeSyntheticEvent, PanResponder} from 'react-native'; +import {FlatList} from 'react-native-gesture-handler'; + +import {fetchUsersByIds} from '@actions/remote/user'; +import {useServerUrl} from '@context/server'; + +import Reactor from './reactor'; + +import type ReactionModel from '@typings/database/models/servers/reaction'; + +type Props = { + reactions: ReactionModel[]; +} + +const ReactorsList = ({reactions}: Props) => { + const serverUrl = useServerUrl(); + const [enabled, setEnabled] = useState(false); + const listRef = useRef(null); + const [direction, setDirection] = useState<'down' | 'up'>('down'); + const prevOffset = useRef(0); + const panResponder = useRef(PanResponder.create({ + onMoveShouldSetPanResponderCapture: (evt, g) => { + const dir = prevOffset.current < g.dy ? 'down' : 'up'; + prevOffset.current = g.dy; + if (!enabled && dir === 'up') { + setEnabled(true); + } + setDirection(dir); + return false; + }, + })).current; + + const renderItem = useCallback(({item}) => ( + + ), [reactions]); + + const onScroll = useCallback((e: NativeSyntheticEvent) => { + if (e.nativeEvent.contentOffset.y <= 0 && enabled && direction === 'down') { + setEnabled(false); + listRef.current?.scrollToOffset({animated: true, offset: 0}); + } + }, [enabled, direction]); + + useEffect(() => { + const userIds = reactions.map((r) => r.userId); + + // Fetch any missing user + fetchUsersByIds(serverUrl, userIds); + }, []); + + return ( + + ); +}; + +export default ReactorsList; + diff --git a/app/screens/reactions/reactors_list/reactor/index.ts b/app/screens/reactions/reactors_list/reactor/index.ts new file mode 100644 index 000000000..3a3a19056 --- /dev/null +++ b/app/screens/reactions/reactors_list/reactor/index.ts @@ -0,0 +1,33 @@ +// 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} from '@constants/database'; +import {WithDatabaseArgs} from '@typings/database/database'; + +import Reactor from './reactor'; + +import type ReactionModel from '@typings/database/models/servers/reaction'; +import type UserModel from '@typings/database/models/servers/user'; + +const {SERVER: {USER}} = MM_TABLES; + +const enhance = withObservables(['reaction'], ({database, reaction}: {reaction: ReactionModel} & WithDatabaseArgs) => { + const user = database.get(USER).query( + Q.where('id', reaction.userId), + Q.take(1), + ).observe().pipe( + switchMap((result) => (result.length ? result[0].observe() : of$(undefined))), + ); + + return { + user, + }; +}); + +export default withDatabase(enhance(Reactor)); diff --git a/app/screens/reactions/reactors_list/reactor/reactor.tsx b/app/screens/reactions/reactors_list/reactor/reactor.tsx new file mode 100644 index 000000000..ab59b878e --- /dev/null +++ b/app/screens/reactions/reactors_list/reactor/reactor.tsx @@ -0,0 +1,33 @@ +// 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 = { + user?: UserModel; +} + +const style = StyleSheet.create({ + container: { + paddingVertical: 0, + paddingHorizontal: 0, + paddingTop: 0, + marginBottom: 8, + }, +}); + +const Reactor = ({user}: Props) => { + return ( + + ); +}; + +export default Reactor; From 41d796e2f8181a42f2d28653e6cd8168df70c6c9 Mon Sep 17 00:00:00 2001 From: Shaz MJ Date: Wed, 23 Mar 2022 08:43:59 +1100 Subject: [PATCH 07/13] Feedback --- app/components/channel_list/categories/body/index.ts | 8 +++++--- app/constants/preferences.ts | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/components/channel_list/categories/body/index.ts b/app/components/channel_list/categories/body/index.ts index 3ca076ef4..368a7f7f8 100644 --- a/app/components/channel_list/categories/body/index.ts +++ b/app/components/channel_list/categories/body/index.ts @@ -89,7 +89,9 @@ const mapPrefName = (prefs: PreferenceModel[]) => of$(prefs.map((p) => p.name)); const mapChannelIds = (channels: ChannelModel[]) => of$(channels.map((c) => c.id)); -const enhance = withObservables(['category'], ({category, locale, database, currentUserId}: {category: CategoryModel; locale: string; currentUserId: string} & WithDatabaseArgs) => { +type EnhanceProps = {category: CategoryModel; locale: string; currentUserId: string} & WithDatabaseArgs + +const enhance = withObservables(['category'], ({category, locale, database, currentUserId}: EnhanceProps) => { const observedCategory = category.observe(); const sortedIds = observedCategory.pipe( switchMap((c) => getSortedIds(database, c, locale)), @@ -112,7 +114,7 @@ const enhance = withObservables(['category'], ({category, locale, database, curr const hiddenGmIds = queryPreferencesByCategory(database, Preferences.CATEGORY_GROUP_CHANNEL_SHOW, undefined, 'false'). observe().pipe(switchMap(mapPrefName)); - let limit = of$(0); + let limit = of$(Preferences.CHANNEL_SIDEBAR_LIMIT_DMS_DEFAULT); if (category.type === 'direct_messages') { limit = queryPreferencesByCategory(database, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_LIMIT_DMS). observe().pipe( @@ -122,7 +124,7 @@ const enhance = withObservables(['category'], ({category, locale, database, curr return of$(parseInt(val[0].value, 10)); } - return of$(0); + return of$(Preferences.CHANNEL_SIDEBAR_LIMIT_DMS_DEFAULT); }, ), ); diff --git a/app/constants/preferences.ts b/app/constants/preferences.ts index c76a1d17b..ba419d49a 100644 --- a/app/constants/preferences.ts +++ b/app/constants/preferences.ts @@ -11,7 +11,6 @@ const Preferences: Record = { CATEGORY_FAVORITE_CHANNEL: 'favorite_channel', CATEGORY_AUTO_RESET_MANUAL_STATUS: 'auto_reset_manual_status', CATEGORY_NOTIFICATIONS: 'notifications', - COMMENTS: 'comments', COMMENTS_ANY: 'any', COMMENTS_ROOT: 'root', @@ -37,6 +36,7 @@ const Preferences: Record = { CATEGORY_SIDEBAR_SETTINGS: 'sidebar_settings', CHANNEL_SIDEBAR_ORGANIZATION: 'channel_sidebar_organization', CHANNEL_SIDEBAR_LIMIT_DMS: 'limit_visible_dms_gms', + CHANNEL_SIDEBAR_LIMIT_DMS_DEFAULT: 20, AUTOCLOSE_DMS_ENABLED: 'after_seven_days', CATEGORY_ADVANCED_SETTINGS: 'advanced_settings', ADVANCED_FILTER_JOIN_LEAVE: 'join_leave', From 735e9a3925a1ff9fed0123a9dd05acc3ef90330e Mon Sep 17 00:00:00 2001 From: Shaz MJ Date: Wed, 23 Mar 2022 12:21:53 +1100 Subject: [PATCH 08/13] Adds mention badges to channel list items --- .../channel/__snapshots__/badge.test.tsx.snap | 38 ++++++++++++ .../categories/body/channel/badge.test.tsx | 20 ++++++ .../categories/body/channel/badge.tsx | 62 +++++++++++++++++++ .../body/channel/channel_list_item.tsx | 11 +++- 4 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 app/components/channel_list/categories/body/channel/__snapshots__/badge.test.tsx.snap create mode 100644 app/components/channel_list/categories/body/channel/badge.test.tsx create mode 100644 app/components/channel_list/categories/body/channel/badge.tsx diff --git a/app/components/channel_list/categories/body/channel/__snapshots__/badge.test.tsx.snap b/app/components/channel_list/categories/body/channel/__snapshots__/badge.test.tsx.snap new file mode 100644 index 000000000..b9aeb9ca7 --- /dev/null +++ b/app/components/channel_list/categories/body/channel/__snapshots__/badge.test.tsx.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/channel_list/categories/body/channel/badge should match snapshot 1`] = ` + + + 10 + + +`; diff --git a/app/components/channel_list/categories/body/channel/badge.test.tsx b/app/components/channel_list/categories/body/channel/badge.test.tsx new file mode 100644 index 000000000..0696f225e --- /dev/null +++ b/app/components/channel_list/categories/body/channel/badge.test.tsx @@ -0,0 +1,20 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {renderWithIntlAndTheme} from '@test/intl-test-helper'; + +import Badge from './badge'; + +describe('components/channel_list/categories/body/channel/badge', () => { + it('should match snapshot', () => { + const wrapper = renderWithIntlAndTheme( + , + ); + expect(wrapper.toJSON()).toMatchSnapshot(); + }); +}); diff --git a/app/components/channel_list/categories/body/channel/badge.tsx b/app/components/channel_list/categories/body/channel/badge.tsx new file mode 100644 index 000000000..7a7270f38 --- /dev/null +++ b/app/components/channel_list/categories/body/channel/badge.tsx @@ -0,0 +1,62 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useMemo} from 'react'; +import {Text, View} from 'react-native'; + +import {useTheme} from '@context/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ + badge: { + backgroundColor: theme.mentionBg, + borderRadius: 10, + height: 20, + marginTop: 5, + paddingHorizontal: 8, + alignItems: 'center', + justifyContent: 'center', + textAlignVertical: 'center', + }, + text: { + color: theme.mentionColor, + ...typography('Body', 75), + }, + mutedBadge: { + backgroundColor: changeOpacity(theme.mentionBg, 0.4), + }, + mutedText: { + color: changeOpacity(theme.mentionColor, 0.4), + }, +})); + +const Badge = ({count, muted}: {count: number; muted: boolean}) => { + const theme = useTheme(); + const styles = getStyleSheet(theme); + + const viewStyle = useMemo(() => [ + styles.badge, + muted && styles.mutedBadge, + ], [muted]); + + const textStyle = useMemo(() => [ + styles.text, + + muted && styles.mutedText, + ], [muted]); + + if (!count) { + return null; + } + + return ( + + + {`${count}`} + + + ); +}; + +export default Badge; diff --git a/app/components/channel_list/categories/body/channel/channel_list_item.tsx b/app/components/channel_list/categories/body/channel/channel_list_item.tsx index f364bd3ec..475035688 100644 --- a/app/components/channel_list/categories/body/channel/channel_list_item.tsx +++ b/app/components/channel_list/categories/body/channel/channel_list_item.tsx @@ -15,6 +15,8 @@ import {useTheme} from '@context/theme'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; +import Badge from './badge'; + import type ChannelModel from '@typings/database/models/servers/channel'; import type MyChannelModel from '@typings/database/models/servers/my_channel'; @@ -65,7 +67,7 @@ const ChannelListItem = ({channel, isActive, isOwnDirectMessage, isMuted, myChan const serverUrl = useServerUrl(); // Make it brighter if it's not muted, and highlighted or has unreads - const bright = !isMuted && (myChannel.isUnread || myChannel.mentionsCount > 0); + const bright = !isMuted && (isActive || myChannel.isUnread || myChannel.mentionsCount > 0); const sharedValue = useSharedValue(collapsed && !bright); @@ -117,6 +119,7 @@ const ChannelListItem = ({channel, isActive, isOwnDirectMessage, isMuted, myChan shared={channel.shared} size={24} type={channel.type} + isMuted={isMuted} /> {displayName} + {myChannel.mentionsCount > 0 && + + } From 54218d8f3db57198fd5552827932b49d9088fec5 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Wed, 23 Mar 2022 10:06:54 -0300 Subject: [PATCH 09/13] Use new observables approach --- app/screens/reactions/index.ts | 10 ++++---- app/screens/reactions/reactions.tsx | 17 ++++++++------ .../reactions/reactors_list/reactor/index.ts | 23 ++++--------------- 3 files changed, 18 insertions(+), 32 deletions(-) diff --git a/app/screens/reactions/index.ts b/app/screens/reactions/index.ts index 0d76d0cd2..0bc13f15c 100644 --- a/app/screens/reactions/index.ts +++ b/app/screens/reactions/index.ts @@ -3,27 +3,25 @@ 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} from '@constants/database'; +import {observePost} from '@queries/servers/post'; import Reactions from './reactions'; import type {WithDatabaseArgs} from '@typings/database/database'; -import type PostModel from '@typings/database/models/servers/post'; type EnhancedProps = WithDatabaseArgs & { postId: string; } -const {POST} = MM_TABLES.SERVER; - const enhanced = withObservables([], ({postId, database}: EnhancedProps) => { - const post = database.get(POST).findAndObserve(postId); + const post = observePost(database, postId); return { reactions: post.pipe( - switchMap((p) => p.reactions.observe()), + switchMap((p) => (p ? p.reactions.observe() : of$(undefined))), ), }; }); diff --git a/app/screens/reactions/reactions.tsx b/app/screens/reactions/reactions.tsx index 6208e1a1e..fd8685416 100644 --- a/app/screens/reactions/reactions.tsx +++ b/app/screens/reactions/reactions.tsx @@ -15,14 +15,14 @@ import type ReactionModel from '@typings/database/models/servers/reaction'; type Props = { initialEmoji: string; - reactions: ReactionModel[]; + reactions?: ReactionModel[]; } const Reactions = ({initialEmoji, reactions}: Props) => { - const [sortedReactions, setSortedReactions] = useState(Array.from(new Set(reactions.map((r) => getEmojiFirstAlias(r.emojiName))))); + const [sortedReactions, setSortedReactions] = useState(Array.from(new Set(reactions?.map((r) => getEmojiFirstAlias(r.emojiName))))); const [index, setIndex] = useState(sortedReactions.indexOf(initialEmoji)); const reactionsByName = useMemo(() => { - return reactions.reduce((acc, reaction) => { + return reactions?.reduce((acc, reaction) => { const emojiAlias = getEmojiFirstAlias(reaction.emojiName); if (acc.has(emojiAlias)) { const rs = acc.get(emojiAlias); @@ -41,6 +41,9 @@ const Reactions = ({initialEmoji, reactions}: Props) => { const renderContent = useCallback(() => { const emojiAlias = sortedReactions[index]; + if (!reactionsByName) { + return null; + } return ( <> @@ -61,11 +64,11 @@ const Reactions = ({initialEmoji, reactions}: Props) => { useEffect(() => { // This helps keep the reactions in the same position at all times until unmounted - const rs = reactions.map((r) => getEmojiFirstAlias(r.emojiName)); + const rs = reactions?.map((r) => getEmojiFirstAlias(r.emojiName)); const sorted = new Set([...sortedReactions]); - const added = rs.filter((r) => !sorted.has(r)); - added.forEach(sorted.add, sorted); - const removed = [...sorted].filter((s) => !rs.includes(s)); + const added = rs?.filter((r) => !sorted.has(r)); + added?.forEach(sorted.add, sorted); + const removed = [...sorted].filter((s) => !rs?.includes(s)); removed.forEach(sorted.delete, sorted); setSortedReactions(Array.from(sorted)); }, [reactions]); diff --git a/app/screens/reactions/reactors_list/reactor/index.ts b/app/screens/reactions/reactors_list/reactor/index.ts index 3a3a19056..148b5a56a 100644 --- a/app/screens/reactions/reactors_list/reactor/index.ts +++ b/app/screens/reactions/reactors_list/reactor/index.ts @@ -1,33 +1,18 @@ // 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} from '@constants/database'; +import {observeUser} from '@app/queries/servers/user'; import {WithDatabaseArgs} from '@typings/database/database'; import Reactor from './reactor'; import type ReactionModel from '@typings/database/models/servers/reaction'; -import type UserModel from '@typings/database/models/servers/user'; -const {SERVER: {USER}} = MM_TABLES; - -const enhance = withObservables(['reaction'], ({database, reaction}: {reaction: ReactionModel} & WithDatabaseArgs) => { - const user = database.get(USER).query( - Q.where('id', reaction.userId), - Q.take(1), - ).observe().pipe( - switchMap((result) => (result.length ? result[0].observe() : of$(undefined))), - ); - - return { - user, - }; -}); +const enhance = withObservables(['reaction'], ({database, reaction}: {reaction: ReactionModel} & WithDatabaseArgs) => ({ + user: observeUser(database, reaction.userId), +})); export default withDatabase(enhance(Reactor)); From 7f9cd287feca18fcf2ae83c7d35410f514b007a2 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Wed, 23 Mar 2022 11:35:37 -0300 Subject: [PATCH 10/13] feedback review --- app/actions/websocket/reactions.ts | 2 +- .../post_list/post/body/reactions/reaction.tsx | 7 +++++-- app/queries/servers/reactions.ts | 15 --------------- .../reactions/reactors_list/reactor/reactor.tsx | 3 --- types/api/users.d.ts | 1 - 5 files changed, 6 insertions(+), 22 deletions(-) delete mode 100644 app/queries/servers/reactions.ts diff --git a/app/actions/websocket/reactions.ts b/app/actions/websocket/reactions.ts index fe3bc8c5c..d922429b3 100644 --- a/app/actions/websocket/reactions.ts +++ b/app/actions/websocket/reactions.ts @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import DatabaseManager from '@database/manager'; -import {queryReaction} from '@queries/servers/reactions'; +import {queryReaction} from '@queries/servers/reaction'; export async function handleAddCustomEmoji(serverUrl: string, msg: WebSocketMessage): Promise { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; diff --git a/app/components/post_list/post/body/reactions/reaction.tsx b/app/components/post_list/post/body/reactions/reaction.tsx index 087c17cef..7527ff0e1 100644 --- a/app/components/post_list/post/body/reactions/reaction.tsx +++ b/app/components/post_list/post/body/reactions/reaction.tsx @@ -18,6 +18,9 @@ type ReactionProps = { theme: Theme; } +const MIN_WIDTH = 50; +const DIGIT_WIDTH = 5; + const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { return { count: { @@ -44,7 +47,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { justifyContent: 'center', marginBottom: 12, marginRight: 8, - minWidth: 50, + minWidth: MIN_WIDTH, }, }; }); @@ -53,7 +56,7 @@ const Reaction = ({count, emojiName, highlight, onPress, onLongPress, theme}: Re const styles = getStyleSheet(theme); const digits = String(count).length; const containerStyle = useMemo(() => { - const minWidth = 50 + (digits * 5); + const minWidth = MIN_WIDTH + (digits * DIGIT_WIDTH); return [styles.reaction, (highlight && styles.highlight), {minWidth}]; }, [styles.reaction, highlight, digits]); diff --git a/app/queries/servers/reactions.ts b/app/queries/servers/reactions.ts deleted file mode 100644 index 8b7176618..000000000 --- a/app/queries/servers/reactions.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {Database, Q} from '@nozbe/watermelondb'; - -import {MM_TABLES} from '@constants/database'; -const {SERVER: {REACTION}} = MM_TABLES; - -export const queryReaction = (database: Database, emojiName: string, postId: string, userId: string) => { - return database.get(REACTION).query( - Q.where('emoji_name', emojiName), - Q.where('post_id', postId), - Q.where('user_id', userId), - ); -}; diff --git a/app/screens/reactions/reactors_list/reactor/reactor.tsx b/app/screens/reactions/reactors_list/reactor/reactor.tsx index ab59b878e..80518f771 100644 --- a/app/screens/reactions/reactors_list/reactor/reactor.tsx +++ b/app/screens/reactions/reactors_list/reactor/reactor.tsx @@ -14,9 +14,6 @@ type Props = { const style = StyleSheet.create({ container: { - paddingVertical: 0, - paddingHorizontal: 0, - paddingTop: 0, marginBottom: 8, }, }); diff --git a/types/api/users.d.ts b/types/api/users.d.ts index 465972da2..09be2f807 100644 --- a/types/api/users.d.ts +++ b/types/api/users.d.ts @@ -43,7 +43,6 @@ type UserProfile = { last_picture_update: number; remote_id?: string; status?: string; - remote_id?: string; }; type UsersState = { From f08ccb2d0cbc5c5b25293c9cde9d93932ac9d57c Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Wed, 23 Mar 2022 15:16:14 -0300 Subject: [PATCH 11/13] feedback review --- app/actions/remote/reactions.ts | 2 +- app/components/post_list/post/body/reactions/reactions.tsx | 2 +- app/screens/reactions/emoji_bar/index.tsx | 2 +- app/screens/reactions/emoji_bar/item.tsx | 2 +- app/screens/reactions/reactors_list/index.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/actions/remote/reactions.ts b/app/actions/remote/reactions.ts index 4b0b481e1..30876d572 100644 --- a/app/actions/remote/reactions.ts +++ b/app/actions/remote/reactions.ts @@ -63,7 +63,7 @@ export const addReaction = async (serverUrl: string, postId: string, emojiName: user_id: currentUserId, post_id: postId, emoji_name: emojiAlias, - create_at: Date.now(), + create_at: 0, } as Reaction, }; } catch (error) { diff --git a/app/components/post_list/post/body/reactions/reactions.tsx b/app/components/post_list/post/body/reactions/reactions.tsx index ff63db352..8abeb0cc7 100644 --- a/app/components/post_list/post/body/reactions/reactions.tsx +++ b/app/components/post_list/post/body/reactions/reactions.tsx @@ -149,7 +149,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled, showModalOverCurrentContext(screen, passProps); } } - }, [intl, postId, theme]); + }, [intl, isTablet, postId, theme]); let addMoreReactions = null; const {reactionsByName, highlightedReactions} = buildReactionsMap(); diff --git a/app/screens/reactions/emoji_bar/index.tsx b/app/screens/reactions/emoji_bar/index.tsx index 092dbbad0..56e639f1d 100644 --- a/app/screens/reactions/emoji_bar/index.tsx +++ b/app/screens/reactions/emoji_bar/index.tsx @@ -60,7 +60,7 @@ const EmojiBar = ({emojiSelected, reactionsByName, setIndex, sortedReactions}: P onPress={onPress} /> ); - }, [sortedReactions, emojiSelected, reactionsByName]); + }, [onPress, emojiSelected, reactionsByName]); useEffect(() => { const t = setTimeout(() => { diff --git a/app/screens/reactions/emoji_bar/item.tsx b/app/screens/reactions/emoji_bar/item.tsx index 3464b4e3b..ecd2467eb 100644 --- a/app/screens/reactions/emoji_bar/item.tsx +++ b/app/screens/reactions/emoji_bar/item.tsx @@ -51,7 +51,7 @@ const Reaction = ({count, emojiName, highlight, onPress}: ReactionProps) => { const handlePress = useCallback(() => { onPress(emojiName); - }, [highlight]); + }, [onPress, emojiName]); return ( { const serverUrl = useServerUrl(); const [enabled, setEnabled] = useState(false); - const listRef = useRef(null); const [direction, setDirection] = useState<'down' | 'up'>('down'); + const listRef = useRef(null); const prevOffset = useRef(0); const panResponder = useRef(PanResponder.create({ onMoveShouldSetPanResponderCapture: (evt, g) => { From ab45851e192f6de7377e4d7d821acf4df65d2292 Mon Sep 17 00:00:00 2001 From: Shaz MJ Date: Thu, 24 Mar 2022 08:19:43 +1100 Subject: [PATCH 12/13] Uses existing badge --- .../__snapshots__/category_body.test.tsx.snap | 6 +- .../categories/body/category_body.tsx | 2 +- .../channel/__snapshots__/badge.test.tsx.snap | 38 ------------ .../channel_list_item.test.tsx.snap | 6 +- .../categories/body/channel/badge.test.tsx | 20 ------ .../categories/body/channel/badge.tsx | 62 ------------------- .../body/channel/channel_list_item.tsx | 31 ++++++---- 7 files changed, 28 insertions(+), 137 deletions(-) delete mode 100644 app/components/channel_list/categories/body/channel/__snapshots__/badge.test.tsx.snap delete mode 100644 app/components/channel_list/categories/body/channel/badge.test.tsx delete mode 100644 app/components/channel_list/categories/body/channel/badge.tsx diff --git a/app/components/channel_list/categories/body/__snapshots__/category_body.test.tsx.snap b/app/components/channel_list/categories/body/__snapshots__/category_body.test.tsx.snap index f8bfadc08..7202ed8da 100644 --- a/app/components/channel_list/categories/body/__snapshots__/category_body.test.tsx.snap +++ b/app/components/channel_list/categories/body/__snapshots__/category_body.test.tsx.snap @@ -13,6 +13,7 @@ Object { Object { "value": Object { "height": 40, + "marginVertical": 2, "opacity": 1, }, } @@ -21,6 +22,7 @@ Object { style={ Object { "height": 40, + "marginVertical": 2, "opacity": 1, } } @@ -46,10 +48,10 @@ Object { diff --git a/app/components/channel_list/categories/body/category_body.tsx b/app/components/channel_list/categories/body/category_body.tsx index c27f2ac00..2961cf21f 100644 --- a/app/components/channel_list/categories/body/category_body.tsx +++ b/app/components/channel_list/categories/body/category_body.tsx @@ -41,7 +41,7 @@ const CategoryBody = ({currentChannelId, sortedIds, category, hiddenChannelIds, collapsed={category.collapsed} /> ); - }, [currentChannelId]); + }, [currentChannelId, category.collapsed]); return ( - - 10 - - -`; diff --git a/app/components/channel_list/categories/body/channel/__snapshots__/channel_list_item.test.tsx.snap b/app/components/channel_list/categories/body/channel/__snapshots__/channel_list_item.test.tsx.snap index 9f969d335..ddfdff33a 100644 --- a/app/components/channel_list/categories/body/channel/__snapshots__/channel_list_item.test.tsx.snap +++ b/app/components/channel_list/categories/body/channel/__snapshots__/channel_list_item.test.tsx.snap @@ -6,6 +6,7 @@ exports[`components/channel_list/categories/body/channel/item should match snaps Object { "value": Object { "height": 40, + "marginVertical": 2, "opacity": 1, }, } @@ -14,6 +15,7 @@ exports[`components/channel_list/categories/body/channel/item should match snaps style={ Object { "height": 40, + "marginVertical": 2, "opacity": 1, } } @@ -39,10 +41,10 @@ exports[`components/channel_list/categories/body/channel/item should match snaps diff --git a/app/components/channel_list/categories/body/channel/badge.test.tsx b/app/components/channel_list/categories/body/channel/badge.test.tsx deleted file mode 100644 index 0696f225e..000000000 --- a/app/components/channel_list/categories/body/channel/badge.test.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import {renderWithIntlAndTheme} from '@test/intl-test-helper'; - -import Badge from './badge'; - -describe('components/channel_list/categories/body/channel/badge', () => { - it('should match snapshot', () => { - const wrapper = renderWithIntlAndTheme( - , - ); - expect(wrapper.toJSON()).toMatchSnapshot(); - }); -}); diff --git a/app/components/channel_list/categories/body/channel/badge.tsx b/app/components/channel_list/categories/body/channel/badge.tsx deleted file mode 100644 index 7a7270f38..000000000 --- a/app/components/channel_list/categories/body/channel/badge.tsx +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {useMemo} from 'react'; -import {Text, View} from 'react-native'; - -import {useTheme} from '@context/theme'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; -import {typography} from '@utils/typography'; - -const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ - badge: { - backgroundColor: theme.mentionBg, - borderRadius: 10, - height: 20, - marginTop: 5, - paddingHorizontal: 8, - alignItems: 'center', - justifyContent: 'center', - textAlignVertical: 'center', - }, - text: { - color: theme.mentionColor, - ...typography('Body', 75), - }, - mutedBadge: { - backgroundColor: changeOpacity(theme.mentionBg, 0.4), - }, - mutedText: { - color: changeOpacity(theme.mentionColor, 0.4), - }, -})); - -const Badge = ({count, muted}: {count: number; muted: boolean}) => { - const theme = useTheme(); - const styles = getStyleSheet(theme); - - const viewStyle = useMemo(() => [ - styles.badge, - muted && styles.mutedBadge, - ], [muted]); - - const textStyle = useMemo(() => [ - styles.text, - - muted && styles.mutedText, - ], [muted]); - - if (!count) { - return null; - } - - return ( - - - {`${count}`} - - - ); -}; - -export default Badge; diff --git a/app/components/channel_list/categories/body/channel/channel_list_item.tsx b/app/components/channel_list/categories/body/channel/channel_list_item.tsx index 475035688..1e458be25 100644 --- a/app/components/channel_list/categories/body/channel/channel_list_item.tsx +++ b/app/components/channel_list/categories/body/channel/channel_list_item.tsx @@ -8,6 +8,7 @@ import {TouchableOpacity} from 'react-native-gesture-handler'; import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import {switchToChannelById} from '@actions/remote/channel'; +import Badge from '@components/badge'; import ChannelIcon from '@components/channel_icon'; import {General} from '@constants'; import {useServerUrl} from '@context/server'; @@ -15,17 +16,15 @@ import {useTheme} from '@context/theme'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; -import Badge from './badge'; - import type ChannelModel from '@typings/database/models/servers/channel'; import type MyChannelModel from '@typings/database/models/servers/my_channel'; const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ container: { flexDirection: 'row', - marginBottom: 8, paddingLeft: 2, - paddingVertical: 4, + height: 40, + alignItems: 'center', }, icon: { fontSize: 24, @@ -44,6 +43,16 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ muted: { color: changeOpacity(theme.sidebarText, 0.4), }, + badge: { + position: 'relative', + borderWidth: 0, + left: 0, + top: 0, + alignSelf: undefined, + }, + mutedBadge: { + opacity: 0.4, + }, })); const textStyle = StyleSheet.create({ @@ -77,6 +86,7 @@ const ChannelListItem = ({channel, isActive, isOwnDirectMessage, isMuted, myChan const animatedStyle = useAnimatedStyle(() => { return { + marginVertical: withTiming(sharedValue.value ? 0 : 2, {duration: 500}), height: withTiming(sharedValue.value ? 0 : 40, {duration: 500}), opacity: withTiming(sharedValue.value ? 0 : 1, {duration: 500, easing: Easing.inOut(Easing.exp)}), }; @@ -87,7 +97,6 @@ const ChannelListItem = ({channel, isActive, isOwnDirectMessage, isMuted, myChan if (channel.type === General.GM_CHANNEL) { return channel.displayName?.split(',').length; } - return 0; }, [channel.type, channel.displayName]); @@ -128,13 +137,11 @@ const ChannelListItem = ({channel, isActive, isOwnDirectMessage, isMuted, myChan > {displayName} - {myChannel.mentionsCount > 0 && - - } - + 0} + value={myChannel.mentionsCount} + style={[styles.badge, isMuted && styles.mutedBadge]} + /> From c0b265132abdcc7951e4be04c830902e384b44a3 Mon Sep 17 00:00:00 2001 From: Shaz MJ Date: Thu, 24 Mar 2022 08:29:35 +1100 Subject: [PATCH 13/13] Removes dupes --- app/queries/servers/channel.ts | 4 ---- app/queries/servers/preference.ts | 14 -------------- 2 files changed, 18 deletions(-) diff --git a/app/queries/servers/channel.ts b/app/queries/servers/channel.ts index 116afdf50..4bce45aeb 100644 --- a/app/queries/servers/channel.ts +++ b/app/queries/servers/channel.ts @@ -307,7 +307,3 @@ export const queryMyChannelSettingsByIds = (database: Database, ids: string[]) = export const queryChannelsByNames = (database: Database, names: string[]) => { return database.get(CHANNEL).query(Q.where('name', Q.oneOf(names))); }; - -export const queryChannelsByNames = (database: Database, names: string[]) => { - return database.get(CHANNEL).query(Q.where('name', Q.oneOf(names))); -}; diff --git a/app/queries/servers/preference.ts b/app/queries/servers/preference.ts index c68657b37..93aa39f41 100644 --- a/app/queries/servers/preference.ts +++ b/app/queries/servers/preference.ts @@ -68,17 +68,3 @@ export const deletePreferences = async (database: ServerDatabase, preferences: P return false; } }; - -export const queryPreferencesByCategory = (database: Database, category: string, name?: string, value?: string) => { - const clauses = [Q.where('category', category)]; - - if (typeof name === 'string') { - clauses.push(Q.where('name', name)); - } - - if (typeof value === 'string') { - clauses.push(Q.where('value', value)); - } - - return database.get(PREFERENCE).query(...clauses); -};