mattermost-mobile/app/actions/helpers/channels.ts
Anurag Shivarathri fedf636b2f
Feat crt (#5455)
* [MM-31133]: participants screen component (#5116)

* Fixed imports

* Misc fixes

* Added i18n string

* Moved '(you)' after the name

* [MM-30386][MM-30387] Client and redux changes for collapsed reply threads (#5175)

* [MM-30386][MM-30387] Client and redux changes for collapsed reply threads

* fix tests

* add and use flat shim

* use test helper function

* use test helper function

* revert crt changes to unused client and action functions

* use toEqual

* incorporate code review feedback

* don't assume server success

* batch actions

* clearer code

* WIP in changes, websockets and more redux changes

* Added mark thread as read and mark all as read in a team

* Added isCollapsedThreadEnabled entity

* Fixed loading global screens on load if it's the last visited screen

* Added root count and websocker and redux events related to threads

* Fixed Message, Mention count root count on init and decrement actions

* Moved old client to new client/rest

* Misc fix

* Misc fix

* Misc.

* Old redux test cases fix

* Added polyfill back for array flat

* Fixes mark as unread and pariticipants error in thread

* Fixed initial load when CRT not enabled

* Handling CRT preference change

* Get threads fallsback to search in posts in case of unfollowed or unloaded threads

* Gets threads on reconnect

* DM, Channel counter fix

* Added threads redux tests

* Fixed channel dot issue when child post is arrived channel not active

* Cleaning up threads on load, not letting posts included in threads getting deleted which can trigger many posts to be loaded at once

* Fixed crash on team switch, partly fixed enable/disable from webapp

* Fix when viewing a post from notification, to prevent from navigating back to global threads

* Fixed when preferences are changed before app is launched

* Fixed issue of NO THREADS appearing on reconnect

* Excludes CRT handling for newly logged in users

* Fixed unread dot showing even when all messages are read

* Update app/mm-redux/selectors/entities/threads.ts

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>

* Not viewing threads action is pushed only when suer is in Global Threads

* Fixed cleanup to exclude current team when cleanup starts on app going to background

* Dispatch is not passed as param, misc code fixes

* Preferences check and handling code refactored

* added redux-mock-store

* Preference change fix

* Removed user login dispatch when handling crt change

* Fixed fallback thread support

* Return getThread

* Added threads to sidebar, moved teamId logic to action

* [MM-29106, MM-34847, MM-34843, MM-34846] Collapsed reply threads, Global Threads & In Channel experience UI implementation (#5376)

* feat: global threads screen

* Refactored global thread menu and misc design fixes

* Added empty states for threads, unread. Removing markdown from thread messages.

* Added friendly date, fixed long usernames & channel names in threads view.

* Mark thread as read

* Added intl, refactored code, seperated thread footer from thread item

* Added intl to global threads, added global threads to android

* Added Follow button to thread view

* Added thread badges

* In Channel experience .. WIP

* Clean posts in channel incase CRT is enabled.

* Added follow button, deleted messages doesnot have footer and design is now according to figma

* Removed old post and post header

* Added for follow/unfollow thread and fixed old one

* MISC design & code fixes

* Added options-follow/unfollow to the post's long press menu

* Shows ORIGINAL MESSAGE DELETED in thread when root post gets deleted

* Post list ts fix

* Old testcases fix

* Eslint fix

* Added custom status emoji in at mention autocomplete

* Fixed the width of the displayname and username

* Changed the max width for the full name and username in the autocomplete item

* Review fixes
Changed the behaviour of ellipses for long names

* Removed safearea bottom

* Fixes Mark and unread and thread header theme

* Fixes mark as unread and pariticipants error in thread

* Unfollowing post fix and misc

* Merge with master fix

* Added fallback to get thread from posts in case of not following/unloaded threads

* Refactored code and added tests for screens

* Loads threads from  global threads screen, marks thread as marked on new thread posts

* Fixed tabs render on no threads, threads footer items styling and tests

* Loads threads based on tab and items according to constant

* Added infinite loading, unreads based on redux and not active thread list

* Fixed the username cutting off after truncating display name issue

* Changed avatars to touchableOpacity and added prop for parent component to style

* Fixed infinite loading, tab switching scroll to top, misc styles, team switching, scroll indicator insets

* Added follow button to DMs, moved follow button further right only on ios

* Snapshot updated

* Moved from useSelector to connect HOC

* Scrolls to top on team change, changed thread constant from mm-redux

* Fix merge issue

* Test case fix

* Fixed participants list

* Update app/components/global_threads/global_threads.tsx

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>

* Update app/components/global_threads/thread_footer/thread_footer.tsx

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>

* Update app/components/sidebars/main/channels_list/channel_item/index.js

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>

* Update app/screens/thread/index.js

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>

* Moved date numbers to constants

* Fixed global threads component issues

* Thread item display logic moved to redux

* Misc fixes

* Reverted to TouchableWithFeedback with opacity

* Removed obselete snapshots

* Removing currentTeamId as we aregetting it in action

* Moved to connect component from useSelect for sidebar

* Thread read issue fix

* Removed react-native-navigation mock

Co-authored-by: Anurag Shivarathri <anurag6713@gmail.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Manoj <manoj@brightscout.com>
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>

Co-authored-by: Anurag Shivarathri <anurag6713@gmail.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
Co-authored-by: Manoj <manoj@brightscout.com>

Co-authored-by: Ashish Bhate <bhate.ashish@gmail.com>
Co-authored-by: Ashish Bhate <ashish.bhate@mattermost.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
Co-authored-by: Manoj <manoj@brightscout.com>
2021-07-21 22:58:04 +05:30

425 lines
15 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ChannelTypes, PreferenceTypes, RoleTypes, UserTypes} from '@mm-redux/action_types';
import {Client4} from '@client/rest';
import {General, Preferences} from '@mm-redux/constants';
import {getCurrentChannelId, getRedirectChannelNameForTeam, getChannelsNameMapInTeam} from '@mm-redux/selectors/entities/channels';
import {getConfig} from '@mm-redux/selectors/entities/general';
import {getMyPreferences} from '@mm-redux/selectors/entities/preferences';
import {getCurrentUserId, getUsers, getUserIdsInChannels} from '@mm-redux/selectors/entities/users';
import {getChannelByName as selectChannelByName, getUserIdFromChannelName, isAutoClosed} from '@mm-redux/utils/channel_utils';
import {getPreferenceKey} from '@mm-redux/utils/preference_utils';
import {ActionResult, GenericAction} from '@mm-redux/types/actions';
import {Channel, ChannelMembership} from '@mm-redux/types/channels';
import {PreferenceType} from '@mm-redux/types/preferences';
import {GlobalState} from '@mm-redux/types/store';
import {UserProfile} from '@mm-redux/types/users';
import {RelationOneToMany} from '@mm-redux/types/utilities';
import {isDirectChannelVisible, isGroupChannelVisible} from '@utils/channels';
import {buildPreference} from '@utils/preferences';
export async function loadSidebarDirectMessagesProfiles(state: GlobalState, channels: Array<Channel>, channelMembers: Array<ChannelMembership>) {
const currentUserId = getCurrentUserId(state);
const usersInChannel: RelationOneToMany<Channel, UserProfile> = getUserIdsInChannels(state);
const directChannels = Object.values(channels).filter((c) => c.type === General.DM_CHANNEL || c.type === General.GM_CHANNEL);
const prefs: Array<PreferenceType> = [];
const promises: Array<Promise<ActionResult>> = []; //only fetch profiles that we don't have and the Direct channel should be visible
const actions = [];
const userIds: Array<string> = [];
// Prepare preferences and start fetching profiles to batch them
directChannels.forEach((c) => {
const profileIds = Array.from(usersInChannel[c.id] || []);
const profilesInChannel: Array<string> = profileIds.filter((u: string) => u !== currentUserId);
userIds.push(...profilesInChannel);
switch (c.type) {
case General.DM_CHANNEL: {
const dm = fetchDirectMessageProfileIfNeeded(state, c, channelMembers, profilesInChannel);
if (dm.preferences.length) {
prefs.push(...dm.preferences);
}
if (dm.promise) {
promises.push(dm.promise);
}
break;
}
case General.GM_CHANNEL: {
const gm = fetchGroupMessageProfilesIfNeeded(state, c, channelMembers, profilesInChannel);
if (gm.preferences.length) {
prefs.push(...gm.preferences);
}
if (gm.promise) {
promises.push(gm.promise);
}
break;
}
}
});
// Save preferences if there are any changes
if (prefs.length) {
Client4.savePreferences(currentUserId, prefs);
actions.push({
type: PreferenceTypes.RECEIVED_PREFERENCES,
data: prefs,
});
}
const profilesAction = await getProfilesFromPromises(promises);
const userIdsSet: Set<string> = new Set(userIds);
if (profilesAction) {
actions.push(profilesAction);
profilesAction.data.forEach((d: any) => {
const {users} = d.data;
users.forEach((u: UserProfile) => userIdsSet.add(u.id));
});
}
if (userIdsSet.size > 0) {
try {
const statuses = await Client4.getStatusesByIds(Array.from(userIdsSet));
if (statuses.length) {
actions.push({
type: UserTypes.RECEIVED_STATUSES,
data: statuses,
});
}
} catch {
// do nothing (status will get fetched later on regardless)
}
}
return actions;
}
export async function fetchMyChannel(channelId: string) {
try {
const data = await Client4.getChannel(channelId);
return {data};
} catch (error) {
return {error};
}
}
export async function fetchMyChannelMember(channelId: string) {
try {
const data = await Client4.getMyChannelMember(channelId);
return {data};
} catch (error) {
return {error};
}
}
export function markChannelAsUnread(state: GlobalState, teamId: string, channelId: string, mentions: Array<string>, isRoot = false): Array<GenericAction> {
const {myMembers} = state.entities.channels;
const {currentUserId} = state.entities.users;
const actions: GenericAction[] = [{
type: ChannelTypes.INCREMENT_TOTAL_MSG_COUNT,
data: {
channelId,
amount: 1,
amountRoot: isRoot ? 1 : 0,
},
}, {
type: ChannelTypes.INCREMENT_UNREAD_MSG_COUNT,
data: {
teamId,
channelId,
amount: 1,
amountRoot: isRoot ? 1 : 0,
onlyMentions: myMembers[channelId] && myMembers[channelId].notify_props &&
myMembers[channelId].notify_props.mark_unread === General.MENTION,
},
}];
if (mentions && mentions.indexOf(currentUserId) !== -1) {
actions.push({
type: ChannelTypes.INCREMENT_UNREAD_MENTION_COUNT,
data: {
teamId,
channelId,
amount: 1,
amountRoot: isRoot ? 1 : 0,
},
});
}
return actions;
}
export function makeDirectChannelVisibleIfNecessary(state: GlobalState, otherUserId: string): GenericAction|null {
const myPreferences = getMyPreferences(state);
const currentUserId = getCurrentUserId(state);
let preference = myPreferences[getPreferenceKey(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, otherUserId)];
if (!preference || preference.value === 'false') {
preference = {
user_id: currentUserId,
category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
name: otherUserId,
value: 'true',
};
Client4.savePreferences(currentUserId, [preference]);
return {
type: PreferenceTypes.RECEIVED_PREFERENCES,
data: [preference],
};
}
return null;
}
export async function makeGroupMessageVisibleIfNecessary(state: GlobalState, channelId: string) {
try {
const myPreferences = getMyPreferences(state);
const currentUserId = getCurrentUserId(state);
let preference = myPreferences[getPreferenceKey(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channelId)];
if (!preference || preference.value === 'false') {
preference = {
user_id: currentUserId,
category: Preferences.CATEGORY_GROUP_CHANNEL_SHOW,
name: channelId,
value: 'true',
};
Client4.savePreferences(currentUserId, [preference]);
const profilesInChannel = await fetchUsersInChannel(state, channelId);
return [{
type: UserTypes.RECEIVED_BATCHED_PROFILES_IN_CHANNEL,
data: [profilesInChannel],
}, {
type: PreferenceTypes.RECEIVED_PREFERENCES,
data: [preference],
}];
}
return null;
} catch {
return null;
}
}
export async function fetchChannelAndMyMember(channelId: string): Promise<Array<GenericAction>> {
const actions: Array<GenericAction> = [];
try {
const [channel, member] = await Promise.all([
Client4.getChannel(channelId),
Client4.getMyChannelMember(channelId),
]);
actions.push({
type: ChannelTypes.RECEIVED_CHANNEL,
data: channel,
},
{
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
data: member,
});
const roles = await Client4.getRolesByNames(member.roles.split(' '));
if (roles.length) {
actions.push({
type: RoleTypes.RECEIVED_ROLES,
data: roles,
});
}
} catch {
// do nothing
}
return actions;
}
export async function getAddedDmUsersIfNecessary(state: GlobalState, preferences: PreferenceType[]): Promise<Array<GenericAction>> {
const userIds: string[] = [];
const actions: Array<GenericAction> = [];
for (const preference of preferences) {
if (preference.category === Preferences.CATEGORY_DIRECT_CHANNEL_SHOW && preference.value === 'true') {
userIds.push(preference.name);
}
}
if (userIds.length !== 0) {
const profiles = getUsers(state);
const currentUserId = getCurrentUserId(state);
const needProfiles: string[] = [];
for (const userId of userIds) {
if (!profiles[userId] && userId !== currentUserId) {
needProfiles.push(userId);
}
}
if (needProfiles.length > 0) {
const data = await Client4.getProfilesByIds(userIds);
if (profiles.lenght) {
actions.push({
type: UserTypes.RECEIVED_PROFILES_LIST,
data,
});
}
}
}
return actions;
}
export function lastChannelIdForTeam(state: GlobalState, teamId: string): string {
const {channels, myMembers} = state.entities.channels;
const {currentUserId} = state.entities.users;
const {myPreferences} = state.entities.preferences;
const lastChannelForTeam = state.views.team.lastChannelForTeam[teamId];
const lastChannelId = lastChannelForTeam && lastChannelForTeam.length ? lastChannelForTeam[0] : '';
const lastChannel = channels[lastChannelId];
const isDMVisible = lastChannel && lastChannel.type === General.DM_CHANNEL &&
isDirectChannelVisible(currentUserId, myPreferences, lastChannel);
const isGMVisible = lastChannel && lastChannel.type === General.GM_CHANNEL &&
isGroupChannelVisible(myPreferences, lastChannel);
if (
myMembers[lastChannelId] &&
lastChannel &&
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)
) {
return lastChannelId;
}
// Fallback to default channel
const channelsInTeam = getChannelsNameMapInTeam(state, teamId);
const channel = selectChannelByName(channelsInTeam, getRedirectChannelNameForTeam(state, teamId));
if (channel) {
return channel.id;
}
// Handle case when the default channel cannot be found
// so we need to get the first available channel of the team
const teamChannels = Object.values(channelsInTeam);
const firstChannel = teamChannels.length ? teamChannels[0].id : '';
return firstChannel;
}
function fetchDirectMessageProfileIfNeeded(state: GlobalState, channel: Channel, channelMembers: Array<ChannelMembership>, profilesInChannel: Array<string>) {
const currentUserId = getCurrentUserId(state);
const myPreferences = getMyPreferences(state);
const users = getUsers(state);
const config = getConfig(state);
const currentChannelId = getCurrentChannelId(state);
const otherUserId = getUserIdFromChannelName(currentUserId, channel.name);
const otherUser = users[otherUserId];
const dmVisible = isDirectChannelVisible(currentUserId, myPreferences, channel);
const dmAutoClosed = isAutoClosed(config, myPreferences, channel, channel.last_post_at, otherUser ? otherUser.delete_at : 0, currentChannelId);
const member = channelMembers.find((cm) => cm.channel_id === channel.id);
const dmIsUnread = member ? member.mention_count > 0 : false;
const dmFetchProfile = dmIsUnread || (dmVisible && !dmAutoClosed);
const preferences = [];
// when then DM is hidden but has new messages
if ((!dmVisible || dmAutoClosed) && dmIsUnread) {
preferences.push(buildPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, currentUserId, otherUserId));
preferences.push(buildPreference(Preferences.CATEGORY_CHANNEL_OPEN_TIME, currentUserId, channel.id, Date.now().toString()));
}
if (dmFetchProfile && !profilesInChannel.includes(otherUserId) && otherUserId !== currentUserId) {
return {
preferences,
promise: fetchUsersInChannel(state, channel.id),
};
}
return {preferences};
}
function fetchGroupMessageProfilesIfNeeded(state: GlobalState, channel: Channel, channelMembers: Array<ChannelMembership>, profilesInChannel: Array<string>) {
const currentUserId = getCurrentUserId(state);
const myPreferences = getMyPreferences(state);
const config = getConfig(state);
const gmVisible = isGroupChannelVisible(myPreferences, channel);
const gmAutoClosed = isAutoClosed(config, myPreferences, channel, channel.last_post_at, 0);
const channelMember = channelMembers.find((cm) => cm.channel_id === channel.id);
let hasMentions = false;
let isUnread = false;
if (channelMember) {
hasMentions = channelMember.mention_count > 0;
isUnread = channelMember.msg_count < channel.total_msg_count;
}
const gmIsUnread = hasMentions || isUnread;
const gmFetchProfile = gmIsUnread || (gmVisible && !gmAutoClosed);
const preferences = [];
// when then GM is hidden but has new messages
if ((!gmVisible || gmAutoClosed) && gmIsUnread) {
preferences.push(buildPreference(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, currentUserId, channel.id));
preferences.push(buildPreference(Preferences.CATEGORY_CHANNEL_OPEN_TIME, currentUserId, channel.id, Date.now().toString()));
}
if (gmFetchProfile && !profilesInChannel.length) {
return {
preferences,
promise: fetchUsersInChannel(state, channel.id),
};
}
return {preferences};
}
async function fetchUsersInChannel(state: GlobalState, channelId: string): Promise<ActionResult> {
try {
const currentUserId = getCurrentUserId(state);
const profiles = await Client4.getProfilesInChannel(channelId);
// When fetching profiles in channels we exclude our own user
const users = profiles.filter((p: UserProfile) => p.id !== currentUserId);
const data = {
channelId,
users,
};
return {data};
} catch (error) {
return {error};
}
}
async function getProfilesFromPromises(promises: Array<Promise<ActionResult>>): Promise<GenericAction | null> {
// Get the profiles returned by the promises
if (!promises.length) {
return null;
}
try {
const result = await Promise.all(promises);
const data = result.filter((p: any) => !p.error);
return {
type: UserTypes.RECEIVED_BATCHED_PROFILES_IN_CHANNEL,
data,
};
} catch {
return null;
}
}