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
519 lines
19 KiB
TypeScript
519 lines
19 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import type {AnyAction} from 'redux';
|
|
import {batchActions} from 'redux-batched-actions';
|
|
|
|
import type {WebSocketMessages} from '@mattermost/client';
|
|
import type {FileInfo} from '@mattermost/types/files';
|
|
import type {Post} from '@mattermost/types/posts';
|
|
import type {ScheduledPost} from '@mattermost/types/schedule_post';
|
|
|
|
import {SearchTypes} from 'mattermost-redux/action_types';
|
|
import {getMyChannelMember} from 'mattermost-redux/actions/channels';
|
|
import * as PostActions from 'mattermost-redux/actions/posts';
|
|
import {createSchedulePost} from 'mattermost-redux/actions/scheduled_posts';
|
|
import * as ThreadActions from 'mattermost-redux/actions/threads';
|
|
import {getChannel, getMyChannelMember as getMyChannelMemberSelector} from 'mattermost-redux/selectors/entities/channels';
|
|
import {makeGetFilesForPost} from 'mattermost-redux/selectors/entities/files';
|
|
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
|
import * as PostSelectors from 'mattermost-redux/selectors/entities/posts';
|
|
import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
|
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
|
import {getCurrentUserId, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
|
|
import {canEditPost, comparePosts} from 'mattermost-redux/utils/post_utils';
|
|
|
|
import {addRecentEmoji, addRecentEmojis} from 'actions/emoji_actions';
|
|
import {setGlobalItem} from 'actions/storage';
|
|
import * as StorageActions from 'actions/storage';
|
|
import {loadNewDMIfNeeded, loadNewGMIfNeeded} from 'actions/user_actions';
|
|
import {removeDraft} from 'actions/views/drafts';
|
|
import {closeModal, openModal} from 'actions/views/modals';
|
|
import * as RhsActions from 'actions/views/rhs';
|
|
import {manuallyMarkThreadAsUnread} from 'actions/views/threads';
|
|
import {getConnectionId} from 'selectors/general';
|
|
import {isEmbedVisible, isInlineImageVisible} from 'selectors/posts';
|
|
import {getSelectedPostId, getSelectedPostCardId, getRhsState} from 'selectors/rhs';
|
|
import {getGlobalItem} from 'selectors/storage';
|
|
|
|
import ReactionLimitReachedModal from 'components/reaction_limit_reached_modal';
|
|
|
|
import {
|
|
ActionTypes,
|
|
Constants,
|
|
ModalIdentifiers,
|
|
RHSStates,
|
|
StoragePrefixes,
|
|
} from 'utils/constants';
|
|
import {matchEmoticons} from 'utils/emoticons';
|
|
import {makeGetIsReactionAlreadyAddedToPost, makeGetUniqueEmojiNameReactionsForPost} from 'utils/post_utils';
|
|
|
|
import type {
|
|
GlobalState,
|
|
DispatchFunc,
|
|
ActionFunc,
|
|
ActionFuncAsync,
|
|
ThunkActionFunc,
|
|
} from 'types/store';
|
|
import type {PostDraft} from 'types/store/draft';
|
|
import type {StorageItem} from 'types/store/storage';
|
|
|
|
import {completePostReceive} from './new_post';
|
|
import type {OnSubmitOptions, SubmitPostReturnType} from './views/create_comment';
|
|
|
|
export type CreatePostOptions = {
|
|
keepDraft?: boolean;
|
|
ignorePostError?: boolean;
|
|
};
|
|
|
|
export function handleNewPost(post: Post, msg?: WebSocketMessages.Posted | WebSocketMessages.EphemeralPost): ActionFuncAsync<boolean> {
|
|
return async (dispatch, getState) => {
|
|
let websocketMessageProps = {};
|
|
const state = getState();
|
|
if (msg) {
|
|
websocketMessageProps = msg.data!;
|
|
}
|
|
|
|
const myChannelMember = getMyChannelMemberSelector(state, post.channel_id);
|
|
const myChannelMemberDoesntExist = !myChannelMember || (Object.keys(myChannelMember).length === 0 && myChannelMember.constructor === Object);
|
|
|
|
if (myChannelMemberDoesntExist) {
|
|
await dispatch(getMyChannelMember(post.channel_id));
|
|
}
|
|
|
|
dispatch(completePostReceive(post, websocketMessageProps, myChannelMemberDoesntExist));
|
|
|
|
if (msg && msg.data && 'channel_type' in msg.data) {
|
|
if (msg.data.channel_type === Constants.DM_CHANNEL) {
|
|
dispatch(loadNewDMIfNeeded(post.channel_id));
|
|
} else if (msg.data.channel_type === Constants.GM_CHANNEL) {
|
|
dispatch(loadNewGMIfNeeded(post.channel_id));
|
|
}
|
|
}
|
|
|
|
return {data: true};
|
|
};
|
|
}
|
|
|
|
const getPostsForIds = PostSelectors.makeGetPostsForIds();
|
|
|
|
export function flagPost(postId: string): ActionFuncAsync {
|
|
return async (dispatch, getState) => {
|
|
await dispatch(PostActions.flagPost(postId));
|
|
const state = getState();
|
|
const rhsState = getRhsState(state);
|
|
|
|
if (rhsState === RHSStates.FLAG) {
|
|
dispatch(addPostToSearchResults(postId));
|
|
}
|
|
|
|
return {data: true};
|
|
};
|
|
}
|
|
|
|
export function unflagPost(postId: string): ActionFuncAsync {
|
|
return async (dispatch, getState) => {
|
|
await dispatch(PostActions.unflagPost(postId));
|
|
const state = getState();
|
|
const rhsState = getRhsState(state);
|
|
|
|
if (rhsState === RHSStates.FLAG) {
|
|
removePostFromSearchResults(postId, state, dispatch);
|
|
}
|
|
|
|
return {data: true};
|
|
};
|
|
}
|
|
|
|
function addRecentEmojisForMessage(message: string): ActionFunc {
|
|
return (dispatch) => {
|
|
// parse message and emit emoji event
|
|
const emojis = matchEmoticons(message);
|
|
if (emojis) {
|
|
const trimmedEmojis = emojis.map((emoji) => emoji.substring(1, emoji.length - 1));
|
|
dispatch(addRecentEmojis(trimmedEmojis));
|
|
}
|
|
return {data: true};
|
|
};
|
|
}
|
|
|
|
export function createPost(
|
|
post: Post,
|
|
files: FileInfo[],
|
|
afterSubmit?: (response: SubmitPostReturnType) => void,
|
|
options?: OnSubmitOptions,
|
|
): ActionFuncAsync<PostActions.CreatePostReturnType> {
|
|
return async (dispatch) => {
|
|
dispatch(addRecentEmojisForMessage(post.message));
|
|
|
|
const result = await dispatch(PostActions.createPost(post, files, afterSubmit));
|
|
|
|
if (!options?.keepDraft) {
|
|
if (post.root_id) {
|
|
dispatch(storeCommentDraft(post.root_id, null));
|
|
} else {
|
|
dispatch(storeDraft(post.channel_id, null));
|
|
}
|
|
}
|
|
|
|
options?.afterOptimisticSubmit?.();
|
|
return result;
|
|
};
|
|
}
|
|
|
|
export function createSchedulePostFromDraft(scheduledPost: ScheduledPost): ActionFuncAsync<PostActions.CreatePostReturnType> {
|
|
return async (dispatch, getState) => {
|
|
dispatch(addRecentEmojisForMessage(scheduledPost.message));
|
|
|
|
const state = getState();
|
|
const connectionId = getConnectionId(state);
|
|
const channel = state.entities.channels.channels[scheduledPost.channel_id];
|
|
const result = await dispatch(createSchedulePost(scheduledPost, channel.team_id, connectionId));
|
|
|
|
return {
|
|
created: !result.error && result.data,
|
|
error: result.error,
|
|
};
|
|
};
|
|
}
|
|
|
|
function storeDraft(channelId: string, draft: null): ActionFunc {
|
|
return (dispatch) => {
|
|
dispatch(StorageActions.setGlobalItem('draft_' + channelId, draft));
|
|
return {data: true};
|
|
};
|
|
}
|
|
|
|
function storeCommentDraft(rootPostId: string, draft: null): ActionFunc {
|
|
return (dispatch) => {
|
|
dispatch(StorageActions.setGlobalItem('comment_draft_' + rootPostId, draft));
|
|
return {data: true};
|
|
};
|
|
}
|
|
|
|
export function submitReaction(postId: string, action: string, emojiName: string): ActionFuncAsync<PostActions.SubmitReactionReturnType> {
|
|
return async (dispatch, getState) => {
|
|
const state = getState();
|
|
const getIsReactionAlreadyAddedToPost = makeGetIsReactionAlreadyAddedToPost();
|
|
|
|
const isReactionAlreadyAddedToPost = getIsReactionAlreadyAddedToPost(state, postId, emojiName);
|
|
|
|
if (action === '+' && !isReactionAlreadyAddedToPost) {
|
|
return dispatch(addReaction(postId, emojiName));
|
|
} else if (action === '-' && isReactionAlreadyAddedToPost) {
|
|
return dispatch(PostActions.removeReaction(postId, emojiName));
|
|
}
|
|
return {error: new Error(`unknown action ${action}`)};
|
|
};
|
|
}
|
|
|
|
export function toggleReaction(postId: string, emojiName: string): ActionFuncAsync<PostActions.SubmitReactionReturnType> {
|
|
return async (dispatch, getState) => {
|
|
const state = getState();
|
|
const getIsReactionAlreadyAddedToPost = makeGetIsReactionAlreadyAddedToPost();
|
|
|
|
const isReactionAlreadyAddedToPost = getIsReactionAlreadyAddedToPost(state, postId, emojiName);
|
|
|
|
if (isReactionAlreadyAddedToPost) {
|
|
return dispatch(PostActions.removeReaction(postId, emojiName));
|
|
}
|
|
return dispatch(addReaction(postId, emojiName));
|
|
};
|
|
}
|
|
|
|
export function addReaction(postId: string, emojiName: string): ActionFuncAsync<PostActions.SubmitReactionReturnType> {
|
|
const getUniqueEmojiNameReactionsForPost = makeGetUniqueEmojiNameReactionsForPost();
|
|
return async (dispatch, getState) => {
|
|
const state = getState();
|
|
const config = getConfig(state);
|
|
const uniqueEmojiNames = getUniqueEmojiNameReactionsForPost(state, postId) ?? [];
|
|
|
|
// If we're adding a new reaction but we're already at or over the limit, stop
|
|
if (uniqueEmojiNames.length >= Number(config.UniqueEmojiReactionLimitPerPost) && !uniqueEmojiNames.some((name) => name === emojiName)) {
|
|
dispatch(openModal({
|
|
modalId: ModalIdentifiers.REACTION_LIMIT_REACHED,
|
|
dialogType: ReactionLimitReachedModal,
|
|
dialogProps: {
|
|
isAdmin: isCurrentUserSystemAdmin(state),
|
|
onExited: () => closeModal(ModalIdentifiers.REACTION_LIMIT_REACHED),
|
|
},
|
|
}));
|
|
return {error: new Error('reached reaction limit')};
|
|
}
|
|
|
|
dispatch(addRecentEmoji(emojiName));
|
|
const result = await dispatch(PostActions.addReaction(postId, emojiName));
|
|
return result;
|
|
};
|
|
}
|
|
|
|
export function searchForTerm(term: string): ActionFunc<boolean> {
|
|
return (dispatch) => {
|
|
dispatch(RhsActions.updateSearchTerms(term));
|
|
dispatch(RhsActions.showSearchResults());
|
|
return {data: true};
|
|
};
|
|
}
|
|
|
|
function addPostToSearchResults(postId: string): ActionFunc {
|
|
return (dispatch, getState) => {
|
|
const state = getState();
|
|
const results = state.entities.search.results;
|
|
const index = results.indexOf(postId);
|
|
if (index === -1) {
|
|
const newPost = PostSelectors.getPost(state, postId);
|
|
const posts = getPostsForIds(state, results).reduce((acc, post) => {
|
|
acc[post.id] = post;
|
|
return acc;
|
|
}, {} as Record<string, Post>);
|
|
posts[newPost.id] = newPost;
|
|
|
|
const newResults = [...results, postId];
|
|
newResults.sort((a, b) => comparePosts(posts[a], posts[b]));
|
|
|
|
dispatch({
|
|
type: SearchTypes.RECEIVED_SEARCH_POSTS,
|
|
data: {posts, order: newResults},
|
|
});
|
|
}
|
|
return {data: true};
|
|
};
|
|
}
|
|
|
|
function removePostFromSearchResults(postId: string, state: GlobalState, dispatch: DispatchFunc) {
|
|
let results = state.entities.search.results;
|
|
const index = results.indexOf(postId);
|
|
if (index > -1) {
|
|
results = [...results];
|
|
results.splice(index, 1);
|
|
|
|
const posts = getPostsForIds(state, results);
|
|
|
|
dispatch({
|
|
type: SearchTypes.RECEIVED_SEARCH_POSTS,
|
|
data: {posts, order: results},
|
|
});
|
|
}
|
|
}
|
|
|
|
export function pinPost(postId: string): ActionFuncAsync<boolean> {
|
|
return async (dispatch, getState) => {
|
|
await dispatch(PostActions.pinPost(postId));
|
|
const state = getState();
|
|
const rhsState = getRhsState(state);
|
|
|
|
if (rhsState === RHSStates.PIN) {
|
|
dispatch(addPostToSearchResults(postId));
|
|
}
|
|
return {data: true};
|
|
};
|
|
}
|
|
|
|
export function unpinPost(postId: string): ActionFuncAsync<boolean> {
|
|
return async (dispatch, getState) => {
|
|
await dispatch(PostActions.unpinPost(postId));
|
|
const state = getState();
|
|
const rhsState = getRhsState(state);
|
|
|
|
if (rhsState === RHSStates.PIN) {
|
|
removePostFromSearchResults(postId, state, dispatch);
|
|
}
|
|
return {data: true};
|
|
};
|
|
}
|
|
|
|
export function setEditingPost(postId = '', refocusId = '', isRHS = false): ActionFunc<boolean, GlobalState> {
|
|
const getFilesForPost = makeGetFilesForPost();
|
|
|
|
return (dispatch, getState) => {
|
|
const state = getState();
|
|
let post = PostSelectors.getPost(state, postId);
|
|
|
|
// getPost selectors doesn't include post's file metadata, so we need to add it manually
|
|
if (post.file_ids?.length) {
|
|
// if the post has files, get their metadata and insert it into the post object
|
|
const files = getFilesForPost(state, postId);
|
|
post = {...post, metadata: {...post.metadata, files}};
|
|
}
|
|
|
|
if (!post || post.pending_post_id === postId) {
|
|
return {data: false};
|
|
}
|
|
|
|
const config = state.entities.general.config;
|
|
const license = state.entities.general.license;
|
|
const userId = getCurrentUserId(state);
|
|
const channel = getChannel(state, post.channel_id);
|
|
const teamId = channel?.team_id || '';
|
|
|
|
const canEdit = canEditPost(state, config, license, teamId, post.channel_id, userId, post);
|
|
|
|
if (!canEdit) {
|
|
return {data: false};
|
|
}
|
|
|
|
const storageKey = `${StoragePrefixes.EDIT_DRAFT}${post.id}`;
|
|
|
|
const actions: AnyAction[] = [{
|
|
type: ActionTypes.TOGGLE_EDITING_POST,
|
|
data: {postId, refocusId, isRHS, show: true},
|
|
}];
|
|
|
|
// We need to see if post's draft is already in store, if it is, we don't need to set it again
|
|
const editDraftInStore = getGlobalItem(state, storageKey, null) as StorageItem<PostDraft>['value'] | null;
|
|
|
|
if (
|
|
!editDraftInStore ||
|
|
(editDraftInStore &&
|
|
editDraftInStore?.message?.length === 0 &&
|
|
editDraftInStore?.fileInfos?.length === 0 &&
|
|
editDraftInStore?.uploadsInProgress?.length === 0
|
|
)
|
|
) {
|
|
actions.push(setGlobalItem(storageKey, post));
|
|
}
|
|
|
|
dispatch(batchActions(actions));
|
|
|
|
return {data: true};
|
|
};
|
|
}
|
|
|
|
export function unsetEditingPost(): ActionFunc<boolean, GlobalState> {
|
|
return (dispatch, getState) => {
|
|
const editingPostId = getState().views.posts.editingPost.postId;
|
|
|
|
const actions: AnyAction[] = [{
|
|
type: ActionTypes.TOGGLE_EDITING_POST,
|
|
data: {
|
|
show: false,
|
|
},
|
|
}];
|
|
|
|
if (editingPostId) {
|
|
const storageKey = `${StoragePrefixes.EDIT_DRAFT}${editingPostId}`;
|
|
actions.push(StorageActions.removeGlobalItem(storageKey));
|
|
}
|
|
|
|
dispatch(batchActions(actions));
|
|
|
|
return {data: true};
|
|
};
|
|
}
|
|
|
|
export function markPostAsUnread(post: Post, location?: string): ActionFuncAsync {
|
|
return async (dispatch, getState) => {
|
|
const state = getState();
|
|
const userId = getCurrentUserId(state);
|
|
const currentTeamId = getCurrentTeamId(state);
|
|
|
|
// if CRT:ON and this is from within ThreadViewer (e.g. post dot-menu), mark the thread as unread and followed
|
|
if (isCollapsedThreadsEnabled(state) && (location === 'RHS_ROOT' || location === 'RHS_COMMENT')) {
|
|
const threadId = post.root_id || post.id;
|
|
ThreadActions.handleFollowChanged(dispatch, threadId, currentTeamId, true);
|
|
dispatch(manuallyMarkThreadAsUnread(threadId, post.create_at - 1));
|
|
await dispatch(ThreadActions.markThreadAsUnread(userId, currentTeamId, threadId, post.id));
|
|
} else {
|
|
// use normal channel unread system
|
|
await dispatch(PostActions.setUnreadPost(userId, post.id));
|
|
}
|
|
|
|
return {data: true};
|
|
};
|
|
}
|
|
|
|
export function markMostRecentPostInChannelAsUnread(channelId: string): ActionFuncAsync {
|
|
return async (dispatch, getState) => {
|
|
let state = getState();
|
|
let postId = PostSelectors.getMostRecentNonSystemPostIdInChannel(state, channelId);
|
|
if (!postId) {
|
|
await dispatch(PostActions.getPosts(channelId));
|
|
state = getState();
|
|
postId = PostSelectors.getMostRecentNonSystemPostIdInChannel(state, channelId);
|
|
}
|
|
if (postId) {
|
|
const lastPost = PostSelectors.getPost(state, postId);
|
|
dispatch(markPostAsUnread(lastPost, 'CENTER'));
|
|
}
|
|
return {data: true};
|
|
};
|
|
}
|
|
|
|
// Action called by DeletePostModal when the post is deleted
|
|
export function deleteAndRemovePost(post: Post): ActionFuncAsync<boolean> {
|
|
return async (dispatch, getState) => {
|
|
const {error} = await dispatch(PostActions.deletePost(post));
|
|
if (error) {
|
|
return {error};
|
|
}
|
|
|
|
if (post.id === getSelectedPostId(getState())) {
|
|
dispatch({
|
|
type: ActionTypes.SELECT_POST,
|
|
postId: '',
|
|
channelId: '',
|
|
timestamp: 0,
|
|
});
|
|
}
|
|
|
|
if (post.id === getSelectedPostCardId(getState())) {
|
|
dispatch({
|
|
type: ActionTypes.SELECT_POST_CARD,
|
|
postId: '',
|
|
channelId: '',
|
|
});
|
|
}
|
|
|
|
if (post.root_id === '') {
|
|
const key = StoragePrefixes.COMMENT_DRAFT + post.id;
|
|
if (getGlobalItem(getState(), key, null)) {
|
|
dispatch(removeDraft(key, post.channel_id, post.id));
|
|
}
|
|
}
|
|
|
|
dispatch(PostActions.removePost(post));
|
|
|
|
return {data: true};
|
|
};
|
|
}
|
|
|
|
export function toggleEmbedVisibility(postId: string): ThunkActionFunc<void> {
|
|
return (dispatch, getState) => {
|
|
const state = getState();
|
|
const currentUserId = getCurrentUserId(state);
|
|
const visible = isEmbedVisible(state, postId);
|
|
|
|
dispatch(StorageActions.setGlobalItem(StoragePrefixes.EMBED_VISIBLE + currentUserId + '_' + postId, !visible));
|
|
};
|
|
}
|
|
|
|
export function resetEmbedVisibility() {
|
|
return StorageActions.actionOnGlobalItemsWithPrefix(StoragePrefixes.EMBED_VISIBLE, () => null);
|
|
}
|
|
|
|
export function toggleInlineImageVisibility(postId: string, imageKey: string): ThunkActionFunc<void> {
|
|
return (dispatch, getState) => {
|
|
const state = getState();
|
|
const currentUserId = getCurrentUserId(state);
|
|
const visible = isInlineImageVisible(state, postId, imageKey);
|
|
|
|
dispatch(StorageActions.setGlobalItem(StoragePrefixes.INLINE_IMAGE_VISIBLE + currentUserId + '_' + postId + '_' + imageKey, !visible));
|
|
};
|
|
}
|
|
|
|
export function resetInlineImageVisibility() {
|
|
return StorageActions.actionOnGlobalItemsWithPrefix(StoragePrefixes.INLINE_IMAGE_VISIBLE, () => null);
|
|
}
|
|
|
|
/*
|
|
* It is called from either center or rhs text input when shortcut for react to last message is pressed
|
|
*
|
|
* @param {string} emittedFrom - It can be either "CENTER", "RHS_ROOT" or "NO_WHERE"
|
|
*/
|
|
export function emitShortcutReactToLastPostFrom(emittedFrom: keyof typeof Constants.Locations) {
|
|
return {
|
|
type: ActionTypes.EMITTED_SHORTCUT_REACT_TO_LAST_POST,
|
|
payload: emittedFrom,
|
|
};
|
|
}
|