mattermost-mobile/app/actions/websocket/index.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

442 lines
18 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {loadChannelsForTeam, setChannelRetryFailed} from '@actions/views/channel';
import {getPostsSince} from '@actions/views/post';
import {loadMe} from '@actions/views/user';
import {WebsocketEvents} from '@constants';
import {ChannelTypes, GeneralTypes, PreferenceTypes, TeamTypes, UserTypes, RoleTypes} from '@mm-redux/action_types';
import {getProfilesByIds, getStatusesByIds} from '@mm-redux/actions/users';
import {Client4} from '@client/rest';
import {getThreads} from '@mm-redux/actions/threads';
import {General} from '@mm-redux/constants';
import {getCurrentChannelId, getCurrentChannelStats} from '@mm-redux/selectors/entities/channels';
import {getConfig} from '@mm-redux/selectors/entities/general';
import {getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
import {isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
import {getCurrentUserId, getUsers, getUserStatuses} from '@mm-redux/selectors/entities/users';
import {ActionResult, DispatchFunc, GenericAction, GetStateFunc, batchActions} from '@mm-redux/types/actions';
import {Channel, ChannelMembership} from '@mm-redux/types/channels';
import {GlobalState} from '@mm-redux/types/store';
import {TeamMembership} from '@mm-redux/types/teams';
import {WebSocketMessage} from '@mm-redux/types/websocket';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
import {removeUserFromList} from '@mm-redux/utils/user_utils';
import websocketClient from '@websocket';
import {
handleChannelConvertedEvent,
handleChannelCreatedEvent,
handleChannelDeletedEvent,
handleChannelMemberUpdatedEvent,
handleChannelSchemeUpdatedEvent,
handleChannelUnarchiveEvent,
handleChannelUpdatedEvent,
handleChannelViewedEvent,
handleDirectAddedEvent,
handleUpdateMemberRoleEvent,
} from './channels';
import {handleConfigChangedEvent, handleLicenseChangedEvent} from './general';
import {handleGroupUpdatedEvent} from './groups';
import {handleOpenDialogEvent} from './integrations';
import {handleNewPostEvent, handlePostDeleted, handlePostEdited, handlePostUnread} from './posts';
import {handlePreferenceChangedEvent, handlePreferencesChangedEvent, handlePreferencesDeletedEvent} from './preferences';
import {handleAddEmoji, handleReactionAddedEvent, handleReactionRemovedEvent} from './reactions';
import {handleRoleAddedEvent, handleRoleRemovedEvent, handleRoleUpdatedEvent} from './roles';
import {handleLeaveTeamEvent, handleUpdateTeamEvent, handleTeamAddedEvent} from './teams';
import {handleThreadUpdated, handleThreadReadChanged, handleThreadFollowChanged} from './threads';
import {handleStatusChangedEvent, handleUserAddedEvent, handleUserRemovedEvent, handleUserRoleUpdated, handleUserUpdatedEvent} from './users';
import {getChannelSinceValue} from '@utils/channels';
import {handleRefreshAppsBindings} from './apps';
export function init(additionalOptions: any = {}) {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
const config = getConfig(getState());
let connUrl = additionalOptions.websocketUrl || config.WebsocketURL || Client4.getUrl();
const authToken = Client4.getToken();
connUrl += `${Client4.getUrlVersion()}/websocket`;
websocketClient.setFirstConnectCallback(() => dispatch(handleFirstConnect()));
websocketClient.setEventCallback((evt: WebSocketMessage) => dispatch(handleEvent(evt)));
websocketClient.setMissedEventsCallback(() => dispatch(doMissedEvents()));
websocketClient.setReconnectCallback(() => dispatch(handleReconnect()));
websocketClient.setCloseCallback((connectFailCount: number) => dispatch(handleClose(connectFailCount)));
const websocketOpts = {
connectionUrl: connUrl,
...additionalOptions,
};
return websocketClient.initialize(authToken, websocketOpts);
};
}
let reconnect = false;
export function close(shouldReconnect = false): GenericAction {
reconnect = shouldReconnect;
websocketClient.close(true);
return {
type: GeneralTypes.WEBSOCKET_CLOSED,
timestamp: Date.now(),
data: null,
};
}
function wsConnected(timestamp = Date.now()) {
return {
type: GeneralTypes.WEBSOCKET_SUCCESS,
timestamp,
data: null,
};
}
export function doFirstConnect(now: number) {
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
const state = getState();
const {lastDisconnectAt} = state.websocket;
const actions: Array<GenericAction> = [wsConnected(now)];
if (isMinimumServerVersion(Client4.getServerVersion(), 5, 14) && lastDisconnectAt) {
const currentUserId = getCurrentUserId(state);
const users = getUsers(state);
const userIds = Object.keys(users);
const userUpdates = await Client4.getProfilesByIds(userIds, {since: lastDisconnectAt});
if (userUpdates.length) {
removeUserFromList(currentUserId, userUpdates);
actions.push({
type: UserTypes.RECEIVED_PROFILES_LIST,
data: userUpdates,
});
}
}
dispatch(batchActions(actions, 'BATCH_WS_CONNCET'));
return {data: true};
};
}
export function doMissedEvents() {
return async (dispatch: DispatchFunc): Promise<ActionResult> => {
dispatch(wsConnected());
return {data: true};
};
}
export function doReconnect(now: number) {
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
const state = getState();
const currentTeamId = getCurrentTeamId(state);
const currentChannelId = getCurrentChannelId(state);
const currentUserId = getCurrentUserId(state);
const users = getUsers(state);
const {lastDisconnectAt} = state.websocket;
const actions: Array<GenericAction> = [];
dispatch(batchActions([
wsConnected(now),
setChannelRetryFailed(false),
], 'BATCH_WS_SUCCESS'));
try {
const {data: me}: any = await dispatch(loadMe(null, null, true));
if (!me.error) {
const roles = [];
if (me.roles?.length) {
roles.push(...me.roles);
}
actions.push({
type: UserTypes.RECEIVED_ME,
data: me.user,
}, {
type: PreferenceTypes.RECEIVED_ALL_PREFERENCES,
data: me.preferences,
}, {
type: TeamTypes.RECEIVED_MY_TEAM_UNREADS,
data: me.teamUnreads,
}, {
type: TeamTypes.RECEIVED_TEAMS_LIST,
data: me.teams,
}, {
type: TeamTypes.RECEIVED_MY_TEAM_MEMBERS,
data: me.teamMembers,
});
const currentTeamMembership = me.teamMembers.find((tm: TeamMembership) => tm.team_id === currentTeamId && tm.delete_at === 0);
if (currentTeamMembership) {
const {data: myData}: any = await dispatch(loadChannelsForTeam(currentTeamId, true, true));
if (myData?.channels && myData?.channelMembers) {
actions.push({
type: ChannelTypes.RECEIVED_MY_CHANNELS_WITH_MEMBERS,
data: myData,
});
if (isCollapsedThreadsEnabled(state)) {
dispatch(getThreads(currentUserId, currentTeamId, '', '', undefined, false, false, (state.websocket?.lastDisconnectAt || Date.now())));
}
const stillMemberOfCurrentChannel = myData.channelMembers.find((cm: ChannelMembership) => cm.channel_id === currentChannelId);
const channelStillExists = myData.channels.find((c: Channel) => c.id === currentChannelId);
const config = me.config || getConfig(getState());
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
if (!stillMemberOfCurrentChannel || !channelStillExists || (!viewArchivedChannels && channelStillExists.delete_at !== 0)) {
EventEmitter.emit(General.SWITCH_TO_DEFAULT_CHANNEL, currentTeamId);
} else {
const postIds = getPostIdsInChannel(state, currentChannelId);
const since = getChannelSinceValue(state, currentChannelId, postIds);
dispatch(getPostsSince(currentChannelId, since));
}
}
if (myData.roles?.length) {
roles.push(...myData.roles);
}
} else {
// If the user is no longer a member of this team when reconnecting
const newMsg = {
data: {
user_id: currentUserId,
team_id: currentTeamId,
},
};
dispatch(handleLeaveTeamEvent(newMsg));
}
if (roles.length) {
actions.push({
type: RoleTypes.RECEIVED_ROLES,
data: roles,
});
}
if (isMinimumServerVersion(Client4.getServerVersion(), 5, 14) && lastDisconnectAt) {
const userIds = Object.keys(users);
const userUpdates = await Client4.getProfilesByIds(userIds, {since: lastDisconnectAt});
if (userUpdates.length) {
removeUserFromList(currentUserId, userUpdates);
actions.push({
type: UserTypes.RECEIVED_PROFILES_LIST,
data: userUpdates,
});
}
}
if (actions.length) {
dispatch(batchActions(actions, 'BATCH_WS_RECONNECT'));
}
}
} catch (e) {
// do nothing
}
return {data: true};
};
}
export function handleUserTypingEvent(msg: WebSocketMessage) {
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
const state = getState();
const currentChannelId = getCurrentChannelId(state);
if (currentChannelId === msg.broadcast.channel_id) {
const profiles = getUsers(state);
const statuses = getUserStatuses(state);
const currentUserId = getCurrentUserId(state);
const config = getConfig(state);
const userId = msg.data.user_id;
const data = {
id: msg.broadcast.channel_id + msg.data.parent_id,
userId,
now: Date.now(),
};
dispatch({
type: WebsocketEvents.TYPING,
data,
});
setTimeout(() => {
const newState = getState();
const {typing} = newState.entities;
if (typing && typing[data.id]) {
dispatch({
type: WebsocketEvents.STOP_TYPING,
data,
});
}
}, parseInt(config.TimeBetweenUserTypingUpdatesMilliseconds!, 10));
if (!profiles[userId] && userId !== currentUserId) {
dispatch(getProfilesByIds([userId]));
}
const status = statuses[userId];
if (status !== General.ONLINE) {
dispatch(getStatusesByIds([userId]));
}
}
return {data: true};
};
}
function handleFirstConnect() {
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
const state = getState();
const config = getConfig(state);
const now = Date.now();
if (reconnect && config?.EnableReliableWebSockets !== 'true') {
reconnect = false;
return dispatch(doReconnect(now));
}
return dispatch(doFirstConnect(now));
};
}
function handleReconnect() {
return (dispatch: DispatchFunc) => {
return dispatch(doReconnect(Date.now()));
};
}
function handleClose(connectFailCount: number) {
return {
type: GeneralTypes.WEBSOCKET_FAILURE,
error: connectFailCount,
data: null,
timestamp: Date.now(),
};
}
function handleEvent(msg: WebSocketMessage) {
return (dispatch: DispatchFunc) => {
switch (msg.event) {
case WebsocketEvents.POSTED:
case WebsocketEvents.EPHEMERAL_MESSAGE:
return dispatch(handleNewPostEvent(msg));
case WebsocketEvents.POST_EDITED:
return dispatch(handlePostEdited(msg));
case WebsocketEvents.POST_DELETED:
return dispatch(handlePostDeleted(msg));
case WebsocketEvents.POST_UNREAD:
return dispatch(handlePostUnread(msg));
case WebsocketEvents.LEAVE_TEAM:
return dispatch(handleLeaveTeamEvent(msg));
case WebsocketEvents.UPDATE_TEAM:
return dispatch(handleUpdateTeamEvent(msg));
case WebsocketEvents.ADDED_TO_TEAM:
return dispatch(handleTeamAddedEvent(msg));
case WebsocketEvents.USER_ADDED:
return dispatch(handleUserAddedEvent(msg));
case WebsocketEvents.USER_REMOVED:
return dispatch(handleUserRemovedEvent(msg));
case WebsocketEvents.USER_UPDATED:
return dispatch(handleUserUpdatedEvent(msg));
case WebsocketEvents.ROLE_ADDED:
return dispatch(handleRoleAddedEvent(msg));
case WebsocketEvents.ROLE_REMOVED:
return dispatch(handleRoleRemovedEvent(msg));
case WebsocketEvents.ROLE_UPDATED:
return dispatch(handleRoleUpdatedEvent(msg));
case WebsocketEvents.USER_ROLE_UPDATED:
return dispatch(handleUserRoleUpdated(msg));
case WebsocketEvents.MEMBERROLE_UPDATED:
return dispatch(handleUpdateMemberRoleEvent(msg));
case WebsocketEvents.CHANNEL_CREATED:
return dispatch(handleChannelCreatedEvent(msg));
case WebsocketEvents.CHANNEL_DELETED:
return dispatch(handleChannelDeletedEvent(msg));
case WebsocketEvents.CHANNEL_UNARCHIVED:
return dispatch(handleChannelUnarchiveEvent(msg));
case WebsocketEvents.CHANNEL_UPDATED:
return dispatch(handleChannelUpdatedEvent(msg));
case WebsocketEvents.CHANNEL_CONVERTED:
return dispatch(handleChannelConvertedEvent(msg));
case WebsocketEvents.CHANNEL_VIEWED:
return dispatch(handleChannelViewedEvent(msg));
case WebsocketEvents.CHANNEL_MEMBER_UPDATED:
return dispatch(handleChannelMemberUpdatedEvent(msg));
case WebsocketEvents.CHANNEL_SCHEME_UPDATED:
return dispatch(handleChannelSchemeUpdatedEvent(msg));
case WebsocketEvents.DIRECT_ADDED:
return dispatch(handleDirectAddedEvent(msg));
case WebsocketEvents.PREFERENCE_CHANGED:
return dispatch(handlePreferenceChangedEvent(msg));
case WebsocketEvents.PREFERENCES_CHANGED:
return dispatch(handlePreferencesChangedEvent(msg));
case WebsocketEvents.PREFERENCES_DELETED:
return dispatch(handlePreferencesDeletedEvent(msg));
case WebsocketEvents.STATUS_CHANGED:
return dispatch(handleStatusChangedEvent(msg));
case WebsocketEvents.TYPING:
return dispatch(handleUserTypingEvent(msg));
case WebsocketEvents.HELLO:
handleHelloEvent(msg);
break;
case WebsocketEvents.REACTION_ADDED:
return dispatch(handleReactionAddedEvent(msg));
case WebsocketEvents.REACTION_REMOVED:
return dispatch(handleReactionRemovedEvent(msg));
case WebsocketEvents.EMOJI_ADDED:
return dispatch(handleAddEmoji(msg));
case WebsocketEvents.LICENSE_CHANGED:
return dispatch(handleLicenseChangedEvent(msg));
case WebsocketEvents.CONFIG_CHANGED:
return dispatch(handleConfigChangedEvent(msg));
case WebsocketEvents.OPEN_DIALOG:
return dispatch(handleOpenDialogEvent(msg));
case WebsocketEvents.RECEIVED_GROUP:
return dispatch(handleGroupUpdatedEvent(msg));
case WebsocketEvents.THREAD_UPDATED:
return dispatch(handleThreadUpdated(msg));
case WebsocketEvents.THREAD_READ_CHANGED:
return dispatch(handleThreadReadChanged(msg));
case WebsocketEvents.THREAD_FOLLOW_CHANGED:
return dispatch(handleThreadFollowChanged(msg));
case WebsocketEvents.APPS_FRAMEWORK_REFRESH_BINDINGS: {
return dispatch(handleRefreshAppsBindings());
}
}
return {data: true};
};
}
function handleHelloEvent(msg: WebSocketMessage) {
const serverVersion = msg.data.server_version;
if (serverVersion && Client4.serverVersion !== serverVersion) {
Client4.serverVersion = serverVersion;
EventEmitter.emit(General.SERVER_VERSION_CHANGED, serverVersion);
}
}
// Helpers
let lastTimeTypingSent = 0;
export function userTyping(state: GlobalState, channelId: string, parentPostId: string): void {
const config = getConfig(state);
const t = Date.now();
const stats = getCurrentChannelStats(state);
const membersInChannel = stats ? stats.member_count : 0;
if (((t - lastTimeTypingSent) > parseInt(config.TimeBetweenUserTypingUpdatesMilliseconds!, 10)) &&
(membersInChannel < parseInt(config.MaxNotificationsPerChannel!, 10)) && (config.EnableUserTypingMessages === 'true')) {
websocketClient.userTyping(channelId, parentPostId);
lastTimeTypingSent = t;
}
}