From 0bbbbb98cb8eb7cc4bcdc77efd8e162939e7fadb Mon Sep 17 00:00:00 2001 From: Mattermost Build Date: Thu, 3 Feb 2022 18:14:35 +0100 Subject: [PATCH] MM-37517 Thread permalink (#5758) (#5925) * Thread permalink fix * Reverted and implemented solution by updating the props * fix TS on selectors/entities/posts.ts Co-authored-by: Mattermod Co-authored-by: Elias Nahum (cherry picked from commit b3283ec9cff0cf1ab84f220ef9fa4fe8719523eb) Co-authored-by: Anurag Shivarathri --- app/actions/views/permalink.ts | 39 ++++++++++++++--- app/mm-redux/actions/threads.ts | 11 ++++- app/mm-redux/selectors/entities/posts.ts | 20 +++++++++ app/screens/permalink/index.js | 22 +++++++--- app/screens/permalink/permalink.js | 56 +++++++++++++++++++++--- app/screens/thread/index.js | 4 +- 6 files changed, 130 insertions(+), 22 deletions(-) diff --git a/app/actions/views/permalink.ts b/app/actions/views/permalink.ts index 98ed2a23a6..5690d6264f 100644 --- a/app/actions/views/permalink.ts +++ b/app/actions/views/permalink.ts @@ -3,10 +3,13 @@ import {intlShape} from 'react-intl'; import {Keyboard} from 'react-native'; +import {Navigation} from 'react-native-navigation'; -import {dismissAllModals, showModalOverCurrentContext} from '@actions/navigation'; +import {showModalOverCurrentContext} from '@actions/navigation'; import {loadChannelsByTeamName} from '@actions/views/channel'; -import {selectFocusedPostId} from '@mm-redux/actions/posts'; +import {getPost as fetchPost, selectFocusedPostId} from '@mm-redux/actions/posts'; +import {getPost} from '@mm-redux/selectors/entities/posts'; +import {isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences'; import {getCurrentTeam} from '@mm-redux/selectors/entities/teams'; import {permalinkBadTeam} from '@utils/general'; import {changeOpacity} from '@utils/theme'; @@ -17,26 +20,50 @@ let showingPermalink = false; export function showPermalink(intl: typeof intlShape, teamName: string, postId: string, openAsPermalink = true) { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState(); + let name = teamName; if (!name) { - name = getCurrentTeam(getState()).name; + name = getCurrentTeam(state).name; } const loadTeam = await dispatch(loadChannelsByTeamName(name, permalinkBadTeam.bind(null, intl))); + let isThreadPost; + + const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state); + if (collapsedThreadsEnabled) { + let post = getPost(state, postId); + if (!post) { + const {data} = await dispatch(fetchPost(postId)); + if (data) { + post = data; + } + } + if (post) { + isThreadPost = Boolean(post.root_id); + } else { + return {}; + } + } + if (!loadTeam.error) { Keyboard.dismiss(); dispatch(selectFocusedPostId(postId)); - if (showingPermalink) { - await dismissAllModals(); - } const screen = 'Permalink'; const passProps = { isPermalink: openAsPermalink, + isThreadPost, + focusedPostId: postId, teamName, }; + if (showingPermalink) { + Navigation.updateProps(screen, passProps); + return {}; + } + const options = { layout: { componentBackgroundColor: changeOpacity('#000', 0.2), diff --git a/app/mm-redux/actions/threads.ts b/app/mm-redux/actions/threads.ts index 6662f42ae6..a57f782251 100644 --- a/app/mm-redux/actions/threads.ts +++ b/app/mm-redux/actions/threads.ts @@ -142,7 +142,16 @@ export function handleThreadArrived(threadData: UserThread, teamId: string) { }, { type: PostTypes.RECEIVED_POSTS, - data: {posts: [{...thread.post, participants: thread.participants}]}, + data: {posts: [{ + + // Merge post data as thread.post might not contain all post data like "metadata" + ...(state.entities.posts.posts[thread.id] || {}), + + ...thread.post, + reply_count: thread.reply_count, + last_reply_at: thread.last_reply_at, + participants: thread.participants, + }]}, }, { type: ThreadTypes.RECEIVED_THREAD, diff --git a/app/mm-redux/selectors/entities/posts.ts b/app/mm-redux/selectors/entities/posts.ts index 06b8da2b7d..a9947c453d 100644 --- a/app/mm-redux/selectors/entities/posts.ts +++ b/app/mm-redux/selectors/entities/posts.ts @@ -78,6 +78,7 @@ export const getPostsInCurrentChannel: (a: GlobalState) => PostWithFormatData[] const getPostsInChannel = makeGetPostsInChannel(); return (state: GlobalState) => getPostsInChannel(state, state.entities.channels.currentChannelId, -1); })(); + export function makeGetPostIdsForThread(): (b: GlobalState, a: $ID) => Array<$ID> { return createIdsSelector( getAllPosts, @@ -101,6 +102,25 @@ export function makeGetPostIdsForThread(): (b: GlobalState, a: $ID) => Arr ); } +export function makeGetPostIdsForThreadWithLimit(): (b: GlobalState, a: $ID, c: string, d: number, e: number) => Array<$ID> { + const getPostIdsForThread = makeGetPostIdsForThread(); + return createIdsSelector( + (state: GlobalState, rootId: string) => getPostIdsForThread(state, rootId), + (state: GlobalState, rootId: string, focusedPostId: string) => focusedPostId, + (state: GlobalState, rootId: string, focusedPostId: string, postsBeforeCount: number) => postsBeforeCount, + (state: GlobalState, rootId: string, focusedPostId: string, postsBeforeCount: number, postsAfterCount: number) => postsAfterCount, + (postIds: Array<$ID>, focusedPostId: string, postsBeforeCount = Posts.POST_CHUNK_SIZE / 2, postsAfterCount = Posts.POST_CHUNK_SIZE / 2) => { + const index = postIds.indexOf(focusedPostId); + if (index > -1) { + const minPostIndex = Math.max(index - postsAfterCount, 0); + const maxPostIndex = Math.min(index + postsBeforeCount + 1, postIds.length); + return postIds.slice(minPostIndex, maxPostIndex); + } + return postIds; + }, + ); +} + export function makeGetPostsChunkAroundPost(): (c: GlobalState, b: $ID, a: $ID) => PostOrderBlock| null | undefined { return createIdsSelector( (state: GlobalState, postId: string, channelId: string) => state.entities.posts.postsInChannel[channelId], diff --git a/app/screens/permalink/index.js b/app/screens/permalink/index.js index 6f497ffb4b..b3225ad776 100644 --- a/app/screens/permalink/index.js +++ b/app/screens/permalink/index.js @@ -12,7 +12,7 @@ import {getChannel as getChannelAction, joinChannel} from '@mm-redux/actions/cha import {selectPost} from '@mm-redux/actions/posts'; import {addUserToTeam, getTeamByName, removeUserFromTeam} from '@mm-redux/actions/teams'; import {makeGetChannel, getMyChannelMemberships} from '@mm-redux/selectors/entities/channels'; -import {makeGetPostIdsAroundPost, getPost} from '@mm-redux/selectors/entities/posts'; +import {makeGetPostIdsAroundPost, getPost, makeGetPostIdsForThreadWithLimit} from '@mm-redux/selectors/entities/posts'; import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {getCurrentTeamId, getTeamByName as selectTeamByName, getTeamMemberships} from '@mm-redux/selectors/entities/teams'; import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; @@ -20,22 +20,31 @@ import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; import Permalink from './permalink'; function makeMapStateToProps() { + const getPostIdsForThread = makeGetPostIdsForThreadWithLimit(); const getPostIdsAroundPost = makeGetPostIdsAroundPost(); const getChannel = makeGetChannel(); return function mapStateToProps(state, props) { - const {currentFocusedPostId} = state.entities.posts; - const post = getPost(state, currentFocusedPostId); + const {focusedPostId} = props; + const post = getPost(state, focusedPostId); let channel; let postIds; if (post) { channel = getChannel(state, {id: post.channel_id}); - postIds = getPostIdsAroundPost(state, currentFocusedPostId, post.channel_id, { + + const options = { postsBeforeCount: 10, postsAfterCount: 10, - }); + }; + + // It is passed only when CRT is enabled and post has a root_id + if (props.isThreadPost) { + postIds = getPostIdsForThread(state, post.root_id, focusedPostId, options.postsBeforeCount, options.postsAfterCount); + } else { + postIds = getPostIdsAroundPost(state, focusedPostId, post.channel_id, options); + } } return { @@ -45,9 +54,10 @@ function makeMapStateToProps() { channelTeamId: channel ? channel.team_id : '', currentTeamId: getCurrentTeamId(state), currentUserId: getCurrentUserId(state), - focusedPostId: currentFocusedPostId, + focusedPostId, myChannelMemberships: getMyChannelMemberships(state), myTeamMemberships: getTeamMemberships(state), + post, postIds, team: selectTeamByName(state, props.teamName), theme: getTheme(state), diff --git a/app/screens/permalink/permalink.js b/app/screens/permalink/permalink.js index 2b60c56a41..686ac26dc9 100644 --- a/app/screens/permalink/permalink.js +++ b/app/screens/permalink/permalink.js @@ -66,8 +66,10 @@ export default class Permalink extends PureComponent { currentUserId: PropTypes.string.isRequired, focusedPostId: PropTypes.string.isRequired, isPermalink: PropTypes.bool, + isThreadPost: PropTypes.bool, myChannelMemberships: PropTypes.object.isRequired, myTeamMemberships: PropTypes.object.isRequired, + post: PropTypes.object, postIds: PropTypes.array, team: PropTypes.object, teamName: PropTypes.string, @@ -106,6 +108,7 @@ export default class Permalink extends PureComponent { this.navigationEventListener = Navigation.events().bindComponent(this); this.mounted = true; + this.focusedPostId = this.props.focusedPostId; if (this.state.loading && this.props.focusedPostId) { this.initialLoad = true; @@ -114,7 +117,13 @@ export default class Permalink extends PureComponent { } componentDidUpdate() { - if (this.state.loading && this.props.focusedPostId && !this.initialLoad) { + let focusedPostIdChanged; + if (this.focusedPostId !== this.props.focusedPostId) { + focusedPostIdChanged = true; + this.focusedPostId = this.props.focusedPostId; + } + + if (focusedPostIdChanged || (this.state.loading && this.props.focusedPostId && !this.initialLoad)) { this.loadPosts(); } } @@ -155,7 +164,7 @@ export default class Permalink extends PureComponent { jumpToChannel = async (channelId) => { if (channelId) { - const {actions, channelId: currentChannelId, channelTeamId, currentTeamId} = this.props; + const {actions, channelId: currentChannelId, channelTeamId, currentTeamId, isThreadPost, post} = this.props; const {closePermalink, handleSelectChannel, handleTeamChange} = actions; actions.selectPost(''); @@ -178,13 +187,20 @@ export default class Permalink extends PureComponent { handleTeamChange(channelTeamId); } - handleSelectChannel(channelId); + await handleSelectChannel(channelId); + + if (isThreadPost) { + EventEmitter.emit('goToThread', { + id: post?.root_id, + channel_id: channelId, + }); + } } }; loadPosts = async () => { const {intl} = this.context; - const {actions, channelId, currentUserId, focusedPostId, isPermalink, postIds} = this.props; + const {actions, channelId, currentUserId, focusedPostId, isPermalink, isThreadPost, postIds} = this.props; const {formatMessage} = intl; let focusChannelId = channelId; @@ -273,7 +289,9 @@ export default class Permalink extends PureComponent { } } - await actions.getPostsAround(focusChannelId, focusedPostId, 10); + if (!isThreadPost) { + await actions.getPostsAround(focusChannelId, focusedPostId, 10); + } if (this.initialLoad) { this.initialLoad = false; @@ -307,7 +325,7 @@ export default class Permalink extends PureComponent { }; render() { - const {channelName, currentUserId, focusedPostId, postIds, theme} = this.props; + const {channelName, currentUserId, focusedPostId, isThreadPost, postIds, theme} = this.props; const {error, joinChannelPromptVisible, loading, retry, title} = this.state; const style = getStyleSheet(theme); @@ -383,7 +401,27 @@ export default class Permalink extends PureComponent { style={style.title} > {this.archivedIcon()} - {title || channelName} + + {title || (isThreadPost ? ( + + ) : channelName)} + + {isThreadPost && ( + <> + {' '} + + + )} @@ -458,6 +496,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { fontSize: 17, fontWeight: '600', }, + description: { + fontSize: 14, + fontWeight: 'normal', + }, postList: { flex: 1, }, diff --git a/app/screens/thread/index.js b/app/screens/thread/index.js index 5b16c3d8dc..d09283dce2 100644 --- a/app/screens/thread/index.js +++ b/app/screens/thread/index.js @@ -26,8 +26,8 @@ function makeMapStateToProps() { const myMember = getMyCurrentChannelMembership(state); const thread = collapsedThreadsEnabled ? getThread(state, ownProps.rootId, true) : null; let lastViewedAt = myMember?.last_viewed_at; - if (collapsedThreadsEnabled && thread) { - lastViewedAt = getThreadLastViewedAt(state, thread.id); + if (collapsedThreadsEnabled) { + lastViewedAt = getThreadLastViewedAt(state, thread?.id); } return { channelId: ownProps.channelId,