Files
upstream-mattermost/webapp/channels/src/actions/global_actions.tsx
upstream-mirror 240db4e2a7
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:29:55 +05:00

438 lines
16 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {batchActions} from 'redux-batched-actions';
import type {Channel, ChannelMembership} from '@mattermost/types/channels';
import type {Post, PostType} from '@mattermost/types/posts';
import type {Team} from '@mattermost/types/teams';
import type {UserProfile} from '@mattermost/types/users';
import {ChannelTypes} from 'mattermost-redux/action_types';
import {fetchAppBindings} from 'mattermost-redux/actions/apps';
import {
fetchChannelsAndMembers,
getChannelByNameAndTeamName,
getChannelStats,
selectChannel,
} from 'mattermost-redux/actions/channels';
import {fetchTeamScheduledPosts} from 'mattermost-redux/actions/scheduled_posts';
import {logout, loadMe} from 'mattermost-redux/actions/users';
import {Preferences} from 'mattermost-redux/constants';
import {appsEnabled} from 'mattermost-redux/selectors/entities/apps';
import {getCurrentChannelStats, getCurrentChannelId, getMyChannelMember, getRedirectChannelNameForTeam, getChannelsNameMapInTeam, getAllDirectChannels, getChannelMessageCount} from 'mattermost-redux/selectors/entities/channels';
import {getConfig, isPerformanceDebuggingEnabled} from 'mattermost-redux/selectors/entities/general';
import {getBool, getIsOnboardingFlowEnabled, isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {isScheduledPostsEnabled} from 'mattermost-redux/selectors/entities/scheduled_posts';
import {getCurrentTeamId, getMyTeams, getTeam, getMyTeamMember, getTeamMemberships, getActiveTeamsList} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUser, getCurrentUserId, isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
import {calculateUnreadCount} from 'mattermost-redux/utils/channel_utils';
import {handleNewPost} from 'actions/post_actions';
import {loadProfilesForSidebar} from 'actions/user_actions';
import {clearUserCookie} from 'actions/views/cookie';
import {close as closeLhs} from 'actions/views/lhs';
import {closeRightHandSide, closeMenu as closeRhsMenu, updateRhsState} from 'actions/views/rhs';
import * as WebsocketActions from 'actions/websocket_actions';
import {getCurrentLocale} from 'selectors/i18n';
import {getIsRhsOpen, getPreviousRhsState, getRhsState} from 'selectors/rhs';
import BrowserStore from 'stores/browser_store';
import LocalStorageStore from 'stores/local_storage_store';
import store from 'stores/redux_store';
import SubMenuModal from 'components/widgets/menu/menu_modals/submenu_modal/submenu_modal';
import WebSocketClient from 'client/web_websocket_client';
import {getHistory} from 'utils/browser_history';
import {ActionTypes, PostTypes, RHSStates, ModalIdentifiers, PreviousViewedTypes} from 'utils/constants';
import DesktopApp from 'utils/desktop_api';
import {filterAndSortTeamsByDisplayName} from 'utils/team_utils';
import * as Utils from 'utils/utils';
import type {ActionFuncAsync, ThunkActionFunc, GlobalState} from 'types/store';
import {openModal} from './views/modals';
const dispatch = store.dispatch;
const getState = store.getState;
export function emitChannelClickEvent(channel: Channel) {
function switchToChannel(chan: Channel) {
const state = getState();
const userId = getCurrentUserId(state);
const teamId = chan.team_id || getCurrentTeamId(state);
const isRHSOpened = getIsRhsOpen(state);
const isPinnedPostsShowing = getRhsState(state) === RHSStates.PIN;
const isChannelFilesShowing = getRhsState(state) === RHSStates.CHANNEL_FILES;
const member = getMyChannelMember(state, chan.id);
const currentChannelId = getCurrentChannelId(state);
const previousRhsState = getPreviousRhsState(state);
dispatch(getChannelStats(chan.id));
const penultimate = LocalStorageStore.getPreviousChannelName(userId, teamId);
const penultimateType = LocalStorageStore.getPreviousViewedType(userId, teamId);
if (penultimate !== chan.name) {
LocalStorageStore.setPenultimateChannelName(userId, teamId, penultimate);
LocalStorageStore.setPreviousChannelName(userId, teamId, chan.name);
}
if (penultimateType !== PreviousViewedTypes.CHANNELS || penultimate !== chan.name) {
LocalStorageStore.setPreviousViewedType(userId, teamId, PreviousViewedTypes.CHANNELS);
LocalStorageStore.setPenultimateViewedType(userId, teamId, penultimateType);
}
// When switching to a different channel if the pinned posts is showing
// Update the RHS state to reflect the pinned post of the selected channel
if (isRHSOpened && isPinnedPostsShowing) {
dispatch(updateRhsState(RHSStates.PIN, chan.id, previousRhsState));
}
if (isRHSOpened && isChannelFilesShowing) {
dispatch(updateRhsState(RHSStates.CHANNEL_FILES, chan.id, previousRhsState));
}
if (currentChannelId) {
loadProfilesForSidebar();
}
dispatch(batchActions([
{
type: ChannelTypes.SELECT_CHANNEL,
data: chan.id,
},
{
type: ActionTypes.SELECT_CHANNEL_WITH_MEMBER,
data: chan.id,
channel: chan,
member: member || {},
},
setLastUnreadChannel(state, chan),
]));
if (appsEnabled(state)) {
dispatch(fetchAppBindings(chan.id));
}
}
switchToChannel(channel);
}
function setLastUnreadChannel(state: GlobalState, channel: Channel) {
const member = getMyChannelMember(state, channel.id);
const messageCount = getChannelMessageCount(state, channel.id);
let hadMentions = false;
let hadUnreads = false;
if (member && messageCount) {
const crtEnabled = isCollapsedThreadsEnabled(state);
const unreadCount = calculateUnreadCount(messageCount, member, crtEnabled);
hadMentions = unreadCount.mentions > 0;
hadUnreads = unreadCount.showUnread && unreadCount.messages > 0;
}
return {
type: ActionTypes.SET_LAST_UNREAD_CHANNEL,
channelId: channel.id,
hadMentions,
hadUnreads,
};
}
export const clearLastUnreadChannel = {
type: ActionTypes.SET_LAST_UNREAD_CHANNEL,
channelId: '',
};
export function updateNewMessagesAtInChannel(channelId: string, lastViewedAt = Date.now()) {
return {
type: ActionTypes.UPDATE_CHANNEL_LAST_VIEWED_AT,
channel_id: channelId,
last_viewed_at: lastViewedAt,
};
}
export function emitCloseRightHandSide() {
dispatch(closeRightHandSide());
}
export function showMobileSubMenuModal(elements: any[]) { // TODO Use more specific type
const submenuModalData = {
modalId: ModalIdentifiers.MOBILE_SUBMENU,
dialogType: SubMenuModal,
dialogProps: {
elements,
},
};
dispatch(openModal(submenuModalData));
}
export function sendEphemeralPost(message: string, channelId?: string, parentId?: string, userId?: string): ActionFuncAsync<boolean> {
return (doDispatch, doGetState) => {
const timestamp = Utils.getTimestamp();
const post: Post = {
id: Utils.generateId(),
user_id: userId || '0',
channel_id: channelId || getCurrentChannelId(doGetState()),
message,
type: PostTypes.EPHEMERAL as PostType,
create_at: timestamp,
update_at: timestamp,
root_id: parentId || '',
props: {},
delete_at: 0,
edit_at: 0,
is_pinned: false,
hashtags: '',
metadata: {
embeds: [],
emojis: [],
files: [],
images: {},
},
original_id: '',
pending_post_id: '',
reply_count: 0,
};
return doDispatch(handleNewPost(post));
};
}
export function sendAddToChannelEphemeralPost(user: UserProfile, addedUsername: string, addedUserId: string, channelId: string, postRootId = '', timestamp: number) {
const post: Post = {
id: Utils.generateId(),
user_id: user.id,
channel_id: channelId || getCurrentChannelId(getState()),
message: '',
type: PostTypes.EPHEMERAL_ADD_TO_CHANNEL as PostType,
create_at: timestamp,
update_at: timestamp,
root_id: postRootId,
props: {
username: user.username,
addedUsername,
addedUserId,
},
edit_at: 0,
delete_at: 0,
is_pinned: false,
original_id: '',
pending_post_id: '',
reply_count: 0,
metadata: {
embeds: [],
emojis: [],
files: [],
images: {},
},
hashtags: '',
};
dispatch(handleNewPost(post));
}
let lastTimeTypingSent = 0;
export function emitLocalUserTypingEvent(channelId: string, parentPostId: string) {
const userTyping: ActionFuncAsync = async (actionDispatch, actionGetState) => {
const state = actionGetState();
const config = getConfig(state);
if (
isPerformanceDebuggingEnabled(state) &&
getBool(state, Preferences.CATEGORY_PERFORMANCE_DEBUGGING, Preferences.NAME_DISABLE_TYPING_MESSAGES)
) {
return {data: false};
}
const t = Date.now();
const stats = getCurrentChannelStats(state);
const membersInChannel = stats ? stats.member_count : 0;
const timeBetweenUserTypingUpdatesMilliseconds = Utils.stringToNumber(config.TimeBetweenUserTypingUpdatesMilliseconds);
const maxNotificationsPerChannel = Utils.stringToNumber(config.MaxNotificationsPerChannel);
if (((t - lastTimeTypingSent) > timeBetweenUserTypingUpdatesMilliseconds) &&
(membersInChannel < maxNotificationsPerChannel) && (config.EnableUserTypingMessages === 'true')) {
WebSocketClient.userTyping(channelId, parentPostId);
lastTimeTypingSent = t;
}
return {data: true};
};
return dispatch(userTyping);
}
export function emitUserLoggedOutEvent(redirectTo = '/', shouldSignalLogout = true, userAction = true) {
// If the logout was intentional, discard knowledge about having previously been logged in.
// This bit is otherwise used to detect session expirations on the login page.
if (userAction) {
LocalStorageStore.setWasLoggedIn(false);
}
dispatch(logout()).then(() => {
if (shouldSignalLogout) {
BrowserStore.signalLogout();
DesktopApp.signalLogout();
}
BrowserStore.clearHideNotificationPermissionRequestBanner();
WebsocketActions.close();
clearUserCookie();
getHistory().push(redirectTo);
}).catch(() => {
getHistory().push(redirectTo);
});
}
export function toggleSideBarRightMenuAction(): ThunkActionFunc<void> {
return (doDispatch) => {
doDispatch(closeRightHandSide());
doDispatch(closeLhs());
doDispatch(closeRhsMenu());
};
}
export function emitBrowserFocus(focus: boolean) {
dispatch({
type: ActionTypes.BROWSER_CHANGE_FOCUS,
focus,
});
}
export async function getTeamRedirectChannelIfIsAccesible(user: UserProfile, team: Team) {
let state = getState();
let channel = null;
const myMember = getMyTeamMember(state, team.id);
if (!myMember || Object.keys(myMember).length === 0) {
return null;
}
let teamChannels = getChannelsNameMapInTeam(state, team.id);
if (!teamChannels || Object.keys(teamChannels).length === 0) {
// This should be executed in pretty limited scenarios (empty teams)
await dispatch(fetchChannelsAndMembers(team.id)); // eslint-disable-line no-await-in-loop
state = getState();
teamChannels = getChannelsNameMapInTeam(state, team.id);
}
const channelName = LocalStorageStore.getPreviousChannelName(user.id, team.id);
channel = teamChannels[channelName];
if (typeof channel === 'undefined') {
const dmList = getAllDirectChannels(state);
channel = dmList.find((directChannel) => directChannel.name === channelName);
}
let channelMember: ChannelMembership | undefined;
if (channel) {
channelMember = getMyChannelMember(state, channel.id);
}
if (!channel || !channelMember) {
// This should be executed in pretty limited scenarios (when the last visited channel in the team has been removed)
await dispatch(getChannelByNameAndTeamName(team.name, channelName)); // eslint-disable-line no-await-in-loop
state = getState();
teamChannels = getChannelsNameMapInTeam(state, team.id);
channel = teamChannels[channelName];
channelMember = getMyChannelMember(state, channel && channel.id);
}
if (!channel || !channelMember) {
const redirectedChannelName = getRedirectChannelNameForTeam(state, team.id);
channel = teamChannels[redirectedChannelName];
channelMember = getMyChannelMember(state, channel && channel.id);
}
if (channel && channelMember) {
return channel;
}
return null;
}
function historyPushWithQueryParams(path: string, queryParams?: URLSearchParams) {
if (queryParams) {
getHistory().push({
pathname: path,
search: queryParams.toString(),
});
} else {
getHistory().push(path);
}
}
export async function redirectUserToDefaultTeam(searchParams?: URLSearchParams) {
let state = getState();
// Assume we need to load the user if they don't have any team memberships loaded or the user loaded
let user = getCurrentUser(state);
const shouldLoadUser = Utils.isEmptyObject(getTeamMemberships(state)) || !user;
const onboardingFlowEnabled = getIsOnboardingFlowEnabled(state);
if (shouldLoadUser) {
await dispatch(loadMe());
state = getState();
user = getCurrentUser(state);
}
if (!user) {
return;
}
// if the user is the first admin
const isUserFirstAdmin = isFirstAdmin(state);
const locale = getCurrentLocale(state);
const teamId = LocalStorageStore.getPreviousTeamId(user.id);
let myTeams = getMyTeams(state);
const teams = getActiveTeamsList(state);
if (teams.length === 0) {
if (isUserFirstAdmin && onboardingFlowEnabled) {
historyPushWithQueryParams('/preparing-workspace', searchParams);
return;
}
historyPushWithQueryParams('/select_team', searchParams);
return;
}
let team: Team | undefined;
if (teamId) {
team = getTeam(state, teamId);
}
if (team && team.delete_at === 0) {
const channel = await getTeamRedirectChannelIfIsAccesible(user, team);
if (channel) {
if (isScheduledPostsEnabled(state)) {
dispatch(fetchTeamScheduledPosts(team.id, true));
}
dispatch(selectChannel(channel.id));
historyPushWithQueryParams(`/${team.name}/channels/${channel.name}`, searchParams);
return;
}
}
myTeams = filterAndSortTeamsByDisplayName(myTeams, locale);
for (const myTeam of myTeams) {
// This should execute async behavior in a pretty limited set of situations, so shouldn't be a problem
const channel = await getTeamRedirectChannelIfIsAccesible(user, myTeam); // eslint-disable-line no-await-in-loop
if (channel) {
dispatch(selectChannel(channel.id));
historyPushWithQueryParams(`/${myTeam.name}/channels/${channel.name}`, searchParams);
return;
}
}
historyPushWithQueryParams('/select_team', searchParams);
}