Files
upstream-jitsi-meet/react/features/chat/reducer.ts
upstream-mirror 1d2bfcac0c
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s
snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
2026-09-15 15:56:32 +05:00

435 lines
12 KiB
TypeScript

import { UPDATE_CONFERENCE_METADATA } from '../base/conference/actionTypes';
import { ILocalParticipant, IParticipant } from '../base/participants/types';
import ReducerRegistry from '../base/redux/ReducerRegistry';
import { ADD_FILE, _FILE_LIST_RECEIVED } from '../file-sharing/actionTypes';
import { IVisitorChatParticipant } from '../visitors/types';
import {
ADD_MESSAGE,
ADD_MESSAGE_REACTION,
CLEAR_CHAT_SEARCH,
CLEAR_CHAT_STATE,
CLOSE_CHAT,
EDIT_MESSAGE,
MODERATE_MESSAGE,
NOTIFY_PRIVATE_RECIPIENTS_CHANGED,
OPEN_CHAT,
REMOVE_LOBBY_CHAT_PARTICIPANT,
RETRACT_MESSAGE,
SET_CHAT_IS_RESIZING,
SET_CHAT_SEARCH_MATCH_INDEX,
SET_CHAT_SEARCH_QUERY,
SET_CHAT_WIDTH,
SET_FOCUSED_TAB,
SET_LOBBY_CHAT_ACTIVE_STATE,
SET_LOBBY_CHAT_RECIPIENT,
SET_MESSAGE_MODERATION_SUPPORTED,
SET_PRIVATE_MESSAGE_RECIPIENT,
SET_USER_CHAT_WIDTH
} from './actionTypes';
import { CHAT_SIZE, ChatTabs } from './constants';
import { addPendingEdit } from './functions';
import { IMessage, IPendingEditsMap } from './types';
const DEFAULT_STATE = {
groupChatWithPermissions: false,
isOpen: false,
messageModerationSupported: false,
messages: [],
notifyPrivateRecipientsChangedTimestamp: undefined,
pendingEdits: {},
reactions: {},
unreadMessagesCount: 0,
unreadFilesCount: 0,
privateMessageRecipient: undefined,
searchQuery: '',
searchMatchIndex: 0,
lobbyMessageRecipient: undefined,
isLobbyChatActive: false,
focusedTab: undefined,
isResizing: false,
width: {
current: CHAT_SIZE,
userSet: null
}
};
export interface IChatState {
focusedTab?: ChatTabs;
groupChatWithPermissions: boolean;
isLobbyChatActive: boolean;
isOpen: boolean;
isResizing: boolean;
lastReadMessage?: IMessage;
lobbyMessageRecipient?: {
id: string;
name: string;
} | ILocalParticipant;
messageModerationSupported: boolean;
messages: IMessage[];
notifyPrivateRecipientsChangedTimestamp?: number;
pendingEdits: IPendingEditsMap;
privateMessageRecipient?: IParticipant | IVisitorChatParticipant;
searchMatchIndex: number;
searchQuery: string;
unreadFilesCount: number;
unreadMessagesCount: number;
width: {
current: number;
userSet: number | null;
};
}
ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, action): IChatState => {
switch (action.type) {
case ADD_MESSAGE: {
const newMessage: IMessage = {
displayName: action.displayName,
editedAt: action.editedAt,
error: action.error,
fileMetadata: action.fileMetadata,
isFromGuest: Boolean(action.isFromGuest),
isFromVisitor: Boolean(action.isFromVisitor),
participantId: action.participantId,
isEdited: Boolean(action.isEdited),
isReaction: action.isReaction,
messageId: action.messageId,
messageType: action.messageType,
message: action.message,
reactions: action.reactions,
privateMessage: action.privateMessage,
lobbyChat: action.lobbyChat,
recipient: action.recipient,
recipientId: action.recipientId,
replyToMessageId: action.replyToMessageId,
sentToVisitor: Boolean(action.sentToVisitor),
timestamp: action.timestamp
};
const pendingEdit = action.messageId ? state.pendingEdits[action.messageId] : undefined;
// only apply a cached pending edit if its claimed author matches
// the author of the message that actually arrived. Prevents a forged/mismatched
// edit that was queued for a not-yet-seen messageId from silently overwriting
// someone else's message once it shows up.
const pendingEditValid = pendingEdit?.participantId === action.participantId;
const finalMessage = pendingEdit && pendingEditValid
? {
...newMessage,
message: pendingEdit.message,
isEdited: true,
editedAt: pendingEdit.editedAt
}
: newMessage;
// React native, unlike web, needs a reverse sorted message list.
const messages = navigator.product === 'ReactNative'
? [
finalMessage,
...state.messages
]
: [
...state.messages,
finalMessage
];
const pendingEdits = { ...state.pendingEdits };
if (pendingEdit && pendingEditValid && action.messageId) {
delete pendingEdits[action.messageId];
}
return {
...state,
lastReadMessage:
action.hasRead ? finalMessage : state.lastReadMessage,
unreadMessagesCount: state.focusedTab !== ChatTabs.CHAT ? state.unreadMessagesCount + 1 : state.unreadMessagesCount,
messages,
pendingEdits
};
}
case ADD_MESSAGE_REACTION: {
const { participantId, reactionList, messageId } = action;
const messages = state.messages.map(message => {
if (messageId === message.messageId) {
const newReactions = new Map(message.reactions);
reactionList.forEach((reaction: string) => {
let participants = newReactions.get(reaction);
if (!participants) {
participants = new Set();
newReactions.set(reaction, participants);
}
participants.add(participantId);
});
return {
...message,
reactions: newReactions
};
}
return message;
});
return {
...state,
messages
};
}
case CLEAR_CHAT_SEARCH:
return {
...state,
searchQuery: '',
searchMatchIndex: 0
};
case CLEAR_CHAT_STATE:
return {
...DEFAULT_STATE,
width: state.width
};
case EDIT_MESSAGE: {
const newMessage = action.message;
let messageExists = false;
let applied = false;
const messages = state.messages.map(m => {
if (m.messageId === newMessage.messageId) {
messageExists = true;
if (!newMessage.participantId || newMessage.participantId !== m.participantId) {
return m;
}
applied = true;
return {
...m,
message: newMessage.message,
isEdited: true,
editedAt: newMessage.editedAt
};
}
return m;
});
// Message hasn't arrived yet — cache it as a pending edit so it can be
// applied later, once ADD_MESSAGE validates authorship.
if (!messageExists) {
return {
...state,
pendingEdits: addPendingEdit(state.pendingEdits, newMessage.messageId, {
message: newMessage.message,
editedAt: newMessage.editedAt,
participantId: newMessage.participantId
})
};
}
if (!applied) {
return state;
}
return {
...state,
messages
};
}
case MODERATE_MESSAGE: {
const messages = state.messages.map(message => {
if (message.messageId === action.messageId) {
return {
...message,
isModerated: true,
moderationReason: action.reason
};
}
return message;
});
return {
...state,
messages
};
}
case RETRACT_MESSAGE: {
const messages = state.messages.map(message => {
if (message.messageId === action.messageId && message.participantId === action.retractedBy) {
return {
...message,
message: '',
isDeleted: true,
retractedBy: action.retractedBy
};
}
return message;
});
return {
...state,
messages
};
}
case SET_MESSAGE_MODERATION_SUPPORTED:
return {
...state,
messageModerationSupported: action.supported
};
case SET_CHAT_SEARCH_MATCH_INDEX:
return {
...state,
searchMatchIndex: action.index
};
case SET_CHAT_SEARCH_QUERY:
return {
...state,
searchQuery: action.query,
searchMatchIndex: 0
};
case SET_PRIVATE_MESSAGE_RECIPIENT:
return {
...state,
privateMessageRecipient: action.participant
};
case OPEN_CHAT:
return {
...state,
isOpen: true,
privateMessageRecipient: action.participant
};
case CLOSE_CHAT:
return {
...state,
isOpen: false,
lastReadMessage: state.messages[
navigator.product === 'ReactNative' ? 0 : state.messages.length - 1],
privateMessageRecipient: action.participant,
isLobbyChatActive: false,
searchQuery: '',
searchMatchIndex: 0
};
case SET_LOBBY_CHAT_RECIPIENT:
return {
...state,
isLobbyChatActive: true,
lobbyMessageRecipient: action.participant,
privateMessageRecipient: undefined,
isOpen: action.open
};
case SET_LOBBY_CHAT_ACTIVE_STATE:
return {
...state,
isLobbyChatActive: action.payload,
isOpen: action.payload || state.isOpen,
privateMessageRecipient: undefined
};
case REMOVE_LOBBY_CHAT_PARTICIPANT:
return {
...state,
messages: state.messages.filter(m => {
if (action.removeLobbyChatMessages) {
return !m.lobbyChat;
}
return true;
}),
isOpen: state.isOpen && state.isLobbyChatActive ? false : state.isOpen,
isLobbyChatActive: false,
lobbyMessageRecipient: undefined
};
case UPDATE_CONFERENCE_METADATA: {
const { metadata } = action;
if (metadata?.permissions) {
return {
...state,
groupChatWithPermissions: Boolean(metadata.permissions.groupChatRestricted)
};
}
break;
}
case SET_FOCUSED_TAB:
return {
...state,
focusedTab: action.tabId,
unreadMessagesCount: action.tabId === ChatTabs.CHAT ? 0 : state.unreadMessagesCount,
unreadFilesCount: action.tabId === ChatTabs.FILE_SHARING ? 0 : state.unreadFilesCount
};
case SET_CHAT_WIDTH: {
return {
...state,
width: {
...state.width,
current: action.width
}
};
}
case SET_USER_CHAT_WIDTH: {
const { width } = action;
return {
...state,
width: {
current: width,
userSet: width
}
};
}
case SET_CHAT_IS_RESIZING: {
return {
...state,
isResizing: action.resizing
};
}
case NOTIFY_PRIVATE_RECIPIENTS_CHANGED:
return {
...state,
notifyPrivateRecipientsChangedTimestamp: action.payload
};
case ADD_FILE:
return {
...state,
unreadFilesCount: action.shouldIncrementUnread ? state.unreadFilesCount + 1 : state.unreadFilesCount
};
case _FILE_LIST_RECEIVED: {
const remoteFilesCount = Object.values(action.files).filter(
(file: any) => file.authorParticipantId !== action.localParticipantId
).length;
return {
...state,
unreadFilesCount: remoteFilesCount
};
}
}
return state;
});