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 <mattermod@users.noreply.github.com>
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
(cherry picked from commit b3283ec9cf)

Co-authored-by: Anurag Shivarathri <anurag6713@gmail.com>
This commit is contained in:
Mattermost Build
2022-02-03 18:14:35 +01:00
committed by GitHub
parent 782dfc85dc
commit 0bbbbb98cb
6 changed files with 130 additions and 22 deletions

View File

@@ -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),

View File

@@ -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,

View File

@@ -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<Post>) => Array<$ID<Post>> {
return createIdsSelector(
getAllPosts,
@@ -101,6 +102,25 @@ export function makeGetPostIdsForThread(): (b: GlobalState, a: $ID<Post>) => Arr
);
}
export function makeGetPostIdsForThreadWithLimit(): (b: GlobalState, a: $ID<Post>, c: string, d: number, e: number) => Array<$ID<Post>> {
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<Post>>, 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<Post>, a: $ID<Channel>) => PostOrderBlock| null | undefined {
return createIdsSelector(
(state: GlobalState, postId: string, channelId: string) => state.entities.posts.postsInChannel[channelId],

View File

@@ -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),

View File

@@ -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 ? (
<FormattedText
id='mobile.routes.thread_crt'
defautMessage='Thread'
/>
) : channelName)}
{isThreadPost && (
<>
{' '}
<FormattedText
id='mobile.routes.thread_crt.in'
defaultMessage='in {channelName}'
style={style.description}
values={{
channelName,
}}
/>
</>
)}
</Text>
</View>
</View>
@@ -458,6 +496,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
fontSize: 17,
fontWeight: '600',
},
description: {
fontSize: 14,
fontWeight: 'normal',
},
postList: {
flex: 1,
},

View File

@@ -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,