snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
This commit is contained in:
434
react/features/chat/reducer.ts
Normal file
434
react/features/chat/reducer.ts
Normal file
@@ -0,0 +1,434 @@
|
||||
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;
|
||||
});
|
||||
Reference in New Issue
Block a user