From ea54a8dff3dadcf671a1f9db6f6a9baa8ed81a91 Mon Sep 17 00:00:00 2001 From: Avinash Lingaloo Date: Wed, 16 Feb 2022 14:21:27 +0400 Subject: [PATCH] MM-41748 Gekidou Post Options Queries (#5972) * skeleton in place * fix ts error * creating base option component * Added all options except reaction * moved options under /component/options * added destructive styling * skeleton - need polishing now * default emojis for quick reaction * rename files and small refactor * Properly close bottom sheet * redid reaction component * canSave, isSaved * canAddReaction condition * fix aligment * code clean up * fix opening on tablet * undo comment on local reaction action * undo needless formatting * clean up comment * shows selected reaction * fix marginTop and added title for Tablet * code clean up * investigating navigation * fixed navigation * Post options bottomSheet and renamed DrawerItem to MenuItem * renamed optionType to testID * update navigation_close_modal to close_bottom * removed context in favor of Pressable * Apply suggestions from code review Co-authored-by: Elias Nahum * removed theme prop from PickReaction * en.json and code fixes * removed post_options from screen/index * removed post_options from screens constant * Revert "removed post_options from screen/index" This reverts commit 24caa9773fef7f5355e8a3231f4b7e7afef2aa1d. * Revert "removed post_options from screens constant" This reverts commit 863e2faaf79819974dbb264d137fdcecc8066ec3. * fix theme import * remove useless margin * disabled post_options * queries - work in progress * queries - work in progress * queries - work in progress * minor fix * queries - work in progress * queries - work in progress * queries - work in progress * queries - work in progress * queries - work in progress * fix query * queries - work in progress * reaction query fixed * queries - work in progress * queries - canReaction option * queries - canDelete option * queries - canReply option * queries - canPin, canSave, canCopyPermalink option * queries - options - wip * queries - options - wip * queries - options - wip * fix location * removed logs * undo post_draft changes Co-authored-by: Elias Nahum --- app/components/post_list/post/index.ts | 2 +- app/components/post_list/post/post.tsx | 3 +- app/constants/preferences.ts | 2 +- .../components/options/base_option.tsx | 11 +- app/screens/post_options/index.ts | 130 +++++++++++++++++- app/screens/post_options/post_options.tsx | 64 ++++----- types/api/config.d.ts | 1 + 7 files changed, 162 insertions(+), 51 deletions(-) diff --git a/app/components/post_list/post/index.ts b/app/components/post_list/post/index.ts index 8640250329..b811622431 100644 --- a/app/components/post_list/post/index.ts +++ b/app/components/post_list/post/index.ts @@ -101,7 +101,7 @@ const withPost = withObservables( const canDelete = from$(hasPermissionForPost(post, currentUser, isOwner ? Permissions.DELETE_POST : Permissions.DELETE_OTHERS_POSTS, false)); const isEphemeral = of$(isPostEphemeral(post)); const isFlagged = database.get(PREFERENCE).query( - Q.where('category', Preferences.CATEGORY_FLAGGED_POST), + Q.where('category', Preferences.CATEGORY_SAVED_POST), Q.where('name', post.id), ).observe().pipe(switchMap((pref) => of$(Boolean(pref.length)))); diff --git a/app/components/post_list/post/post.tsx b/app/components/post_list/post/post.tsx index fc0cf921e6..49f4b5b2cd 100644 --- a/app/components/post_list/post/post.tsx +++ b/app/components/post_list/post/post.tsx @@ -165,8 +165,7 @@ const Post = ({ } Keyboard.dismiss(); - - const passProps = {location, post}; + const passProps = {location, post, showAddReaction}; const title = isTablet ? intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}) : ''; if (isTablet) { diff --git a/app/constants/preferences.ts b/app/constants/preferences.ts index 5334e5e857..5522872139 100644 --- a/app/constants/preferences.ts +++ b/app/constants/preferences.ts @@ -7,7 +7,7 @@ const Preferences: Record = { CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show', CATEGORY_GROUP_CHANNEL_SHOW: 'group_channel_show', CATEGORY_EMOJI: 'emoji', - CATEGORY_FLAGGED_POST: 'flagged_post', + CATEGORY_SAVED_POST: 'flagged_post', CATEGORY_FAVORITE_CHANNEL: 'favorite_channel', CATEGORY_AUTO_RESET_MANUAL_STATUS: 'auto_reset_manual_status', CATEGORY_NOTIFICATIONS: 'notifications', diff --git a/app/screens/post_options/components/options/base_option.tsx b/app/screens/post_options/components/options/base_option.tsx index 9c4bbe9a72..10f5342295 100644 --- a/app/screens/post_options/components/options/base_option.tsx +++ b/app/screens/post_options/components/options/base_option.tsx @@ -42,20 +42,13 @@ const BaseOption = ({ const theme = useTheme(); const styles = getStyleSheet(theme); - const labelStyles = useMemo(() => { - if (isDestructive) { - return [styles.label, styles.destructive]; - } - return styles.label; - }, [isDestructive, styles.label, styles.destructive]); - const label = useMemo(() => ( - ), [i18nId, defaultMessage, labelStyles]); + ), [i18nId, defaultMessage, theme]); return ( { + if (!post || isSystemMessage(post)) { + return false; + } + + let cep: boolean; + + const permissions = [Permissions.EDIT_POST]; + if (!isOwner) { + permissions.push(Permissions.EDIT_OTHERS_POSTS); + } + + cep = permissions.every((permission) => hasPermissionForChannel(channel, user, permission, false)); + if (isLicensed && postEditTimeLimit !== -1) { + const timeLeft = (post.createAt + (postEditTimeLimit * 1000)) - Date.now(); + if (timeLeft <= 0) { + cep = false; + } + } + + return cep; +}; + +const enhanced = withObservables([], ({post, showAddReaction, location, database}: WithDatabaseArgs & { post: PostModel; showAddReaction: boolean; location: string }) => { + const channel = post.channel.observe(); + const channelIsArchived = channel.pipe(switchMap((ch: ChannelModel) => of$(ch.deleteAt !== 0))); + const currentUser = database.get(SYSTEM).findAndObserve(CURRENT_USER_ID).pipe(switchMap(({value}) => database.get(USER).findAndObserve(value))); + const config = database.get(SYSTEM).findAndObserve(CONFIG).pipe(switchMap(({value}) => of$(value as ClientConfig))); + const isLicensed = database.get(SYSTEM).findAndObserve(LICENSE).pipe(switchMap(({value}) => of$(value.IsLicensed === 'true'))); + const allowEditPost = config.pipe(switchMap((cfg) => of$(cfg.AllowEditPost))); + const serverVersion = config.pipe(switchMap((cfg) => cfg.Version)); + const postEditTimeLimit = config.pipe(switchMap((cfg) => of$(parseInt(cfg.PostEditTimeLimit || '-1', 10)))); + + const canPostPermission = combineLatest([channel, currentUser]).pipe(switchMap(([c, u]) => from$(hasPermissionForChannel(c, u, Permissions.CREATE_POST, false)))); + const hasAddReactionPermission = currentUser.pipe(switchMap((u) => from$(hasPermissionForPost(post, u, Permissions.ADD_REACTION, true)))); + const canDeletePostPermission = currentUser.pipe(switchMap((u) => { + const isOwner = post.userId === u.id; + return from$(hasPermissionForPost(post, u, isOwner ? Permissions.DELETE_POST : Permissions.DELETE_OTHERS_POSTS, false)); + })); + + const experimentalTownSquareIsReadOnly = config.pipe(switchMap((value) => of$(value.ExperimentalTownSquareIsReadOnly === 'true'))); + const channelIsReadOnly = combineLatest([currentUser, channel, experimentalTownSquareIsReadOnly]).pipe(switchMap(([u, c, readOnly]) => { + return of$(c?.name === General.DEFAULT_CHANNEL && !isSystemAdmin(u.roles) && readOnly); + })); + + const isUnderMaxAllowedReactions = post.reactions.observe().pipe( + // eslint-disable-next-line max-nested-callbacks + switchMap((reactions: ReactionModel[]) => of$(new Set(reactions.map((r) => r.emojiName)).size < MAX_ALLOWED_REACTIONS)), + ); + + const canEditUntil = combineLatest([isLicensed, allowEditPost, postEditTimeLimit, serverVersion, channelIsArchived, channelIsReadOnly]).pipe( + switchMap(([ls, alw, limit, semVer, isArchived, isReadOnly]) => { + if (!isArchived && !isReadOnly && ls && ((alw === Permissions.ALLOW_EDIT_POST_TIME_LIMIT && !isMinimumServerVersion(semVer, 6)) || (limit !== -1))) { + return of$(post.createAt + (limit * (1000))); + } + return of$(-1); + }), + ); + + const canReply = combineLatest([channelIsArchived, channelIsReadOnly, canPostPermission]).pipe(switchMap(([isArchived, isReadOnly, canPost]) => { + return of$(!isArchived && !isReadOnly && location !== Screens.THREAD && !isSystemMessage(post) && canPost); + })); + + const canPin = combineLatest([channelIsArchived, channelIsReadOnly]).pipe(switchMap(([isArchived, isReadOnly]) => { + return of$(!isSystemMessage(post) && !isArchived && !isReadOnly); + })); + + const isSaved = database.get(PREFERENCE).query(Q.where('category', Preferences.CATEGORY_SAVED_POST), Q.where('name', post.id)).observe().pipe(switchMap((pref) => of$(Boolean(pref[0]?.value === 'true')))); + + const canEdit = combineLatest([postEditTimeLimit, isLicensed, channel, currentUser, channelIsArchived, channelIsReadOnly, canEditUntil, canPostPermission]).pipe(switchMap(([lt, ls, c, u, isArchived, isReadOnly, until, canPost]) => { + const isOwner = u.id === post.userId; + const canEditPostPermission = canEditPost(isOwner, post, lt, ls, c, u); + const timeReached = until === -1 || until > Date.now(); + return of$(canEditPostPermission && isSystemMessage(post) && !isArchived && !isReadOnly && !timeReached && canPost); + })); + + const canMarkAsUnread = combineLatest([currentUser, channelIsArchived]).pipe( + switchMap(([user, isArchived]) => of$(!isArchived && user.id !== post.userId && !isSystemMessage(post))), + ); + + const canAddReaction = combineLatest([hasAddReactionPermission, channelIsReadOnly, isUnderMaxAllowedReactions, channelIsArchived]).pipe( + switchMap(([permission, readOnly, maxAllowed, isArchived]) => { + return of$(!isSystemMessage(post) && permission && !readOnly && !isArchived && maxAllowed && showAddReaction); + }), + ); + + const canDelete = combineLatest([canDeletePostPermission, channelIsArchived, channelIsReadOnly, canPostPermission]).pipe(switchMap(([permission, isArchived, isReadOnly, canPost]) => { + return of$(permission && !isArchived && !isReadOnly && canPost); + })); + + return { + canMarkAsUnread, + canAddReaction, + canDelete, + canReply, + canPin, + isSaved, + canEdit, + post, + }; +}); + +export default withDatabase(enhanced(PostOptions)); + diff --git a/app/screens/post_options/post_options.tsx b/app/screens/post_options/post_options.tsx index 0330cf5049..cb561f978a 100644 --- a/app/screens/post_options/post_options.tsx +++ b/app/screens/post_options/post_options.tsx @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {useManagedConfig} from '@mattermost/react-native-emm'; import React from 'react'; import {ITEM_HEIGHT} from '@components/menu_item'; @@ -21,48 +22,43 @@ import ReactionBar from './components/reaction_bar'; import type PostModel from '@typings/database/models/servers/post'; -//fixme: some props are optional - review them - type PostOptionsProps = { - canAddReaction?: boolean; - canCopyPermalink?: boolean; - canCopyText?: boolean; - canDelete?: boolean; - canEdit?: boolean; - canEditUntil?: number; - canMarkAsUnread?: boolean; - canPin?: boolean; - canSave?: boolean; - canReply?: boolean; - isSaved?: boolean; + canAddReaction: boolean; + canDelete: boolean; + canEdit: boolean; + canMarkAsUnread: boolean; + canPin: boolean; + canReply: boolean; + isSaved: boolean; location: typeof Screens[keyof typeof Screens]; post: PostModel; - thread?: Partial; + thread: Partial; }; const PostOptions = ({ - canAddReaction = true, - canCopyPermalink = true, - canCopyText = true, - canDelete = true, - canEdit = true, - canEditUntil = -1, - canMarkAsUnread = true, - canPin = true, - canReply = true, - canSave = true, - isSaved = true, + canAddReaction, + canDelete, + canEdit, + canMarkAsUnread, + canPin, + canReply, + isSaved, location, post, thread, }: PostOptionsProps) => { - const shouldRenderEdit = canEdit && (canEditUntil === -1 || canEditUntil > Date.now()); + const managedConfig = useManagedConfig(); + const isSystemPost = isSystemMessage(post); + + const canCopyPermalink = !isSystemPost && managedConfig?.copyAndPasteProtection !== 'true'; + const canCopyText = canCopyPermalink && post.message; + const shouldRenderFollow = !(location !== Screens.CHANNEL || !thread); const snapPoints = [ canAddReaction, canCopyPermalink, canCopyText, - canDelete, shouldRenderEdit, shouldRenderFollow, - canMarkAsUnread, canPin, canReply, canSave, + canDelete, canEdit, shouldRenderFollow, + canMarkAsUnread, canPin, canReply, !isSystemPost, ].reduce((acc, v) => { return v ? acc + 1 : acc; }, 0); @@ -78,18 +74,12 @@ const PostOptions = ({ thread={thread} /> } - {canMarkAsUnread && !isSystemMessage(post) && ( - - )} + {canMarkAsUnread && !isSystemPost && ()} {canCopyPermalink && } - {canSave && - - } + {!isSystemPost && } {canCopyText && } {canPin && } - {shouldRenderEdit && } + {canEdit && } {canDelete && } ); diff --git a/types/api/config.d.ts b/types/api/config.d.ts index 5c5fbb64b4..eae4658d83 100644 --- a/types/api/config.d.ts +++ b/types/api/config.d.ts @@ -5,6 +5,7 @@ interface ClientConfig { AboutLink: string; AllowBannerDismissal: string; AllowCustomThemes: string; + AllowEditPost: string; AllowedThemes: string; AndroidAppDownloadLink: string; AndroidLatestVersion: string;