mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 11:22:31 +00:00
Introduces a comprehensive disableChat config option that disables the entire chat feature including button visibility, notifications, sounds, private messages, and keyboard shortcuts. When disabled, the chat tab is hidden from the chat panel while allowing other tabs (polls, files, CC) to remain accessible.
810 lines
27 KiB
TypeScript
810 lines
27 KiB
TypeScript
import { AnyAction } from 'redux';
|
|
|
|
import { IReduxState, IStore } from '../app/types';
|
|
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
|
|
import {
|
|
CONFERENCE_JOINED,
|
|
ENDPOINT_MESSAGE_RECEIVED,
|
|
NON_PARTICIPANT_MESSAGE_RECEIVED
|
|
} from '../base/conference/actionTypes';
|
|
import { getCurrentConference } from '../base/conference/functions';
|
|
import { IJitsiConference } from '../base/conference/reducer';
|
|
import { openDialog } from '../base/dialog/actions';
|
|
import i18next from '../base/i18n/i18next';
|
|
import {
|
|
JitsiConferenceErrors,
|
|
JitsiConferenceEvents
|
|
} from '../base/lib-jitsi-meet';
|
|
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT, PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
|
|
import {
|
|
getLocalParticipant,
|
|
getParticipantById,
|
|
getParticipantDisplayName
|
|
} from '../base/participants/functions';
|
|
import { IParticipant } from '../base/participants/types';
|
|
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
|
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
|
import { playSound, registerSound, unregisterSound } from '../base/sounds/actions';
|
|
import { arePollsDisabled } from '../conference/functions.any';
|
|
import { isFileSharingEnabled } from '../file-sharing/functions.any';
|
|
import { addGif } from '../gifs/actions';
|
|
import { extractGifURL, getGifDisplayMode, isGifEnabled, isGifMessage } from '../gifs/function.any';
|
|
import { showMessageNotification } from '../notifications/actions';
|
|
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
|
|
import { resetUnreadPollsCount } from '../polls/actions';
|
|
import { ADD_REACTION_MESSAGE } from '../reactions/actionTypes';
|
|
import { pushReactions } from '../reactions/actions.any';
|
|
import { ENDPOINT_REACTION_NAME } from '../reactions/constants';
|
|
import { getReactionMessageFromBuffer, isReactionsEnabled } from '../reactions/functions.any';
|
|
import { isCCTabEnabled } from '../subtitles/functions.any';
|
|
import { showToolbox } from '../toolbox/actions';
|
|
import { getDisplayName } from '../visitors/functions';
|
|
|
|
import {
|
|
ADD_MESSAGE,
|
|
CLOSE_CHAT,
|
|
OPEN_CHAT,
|
|
SEND_MESSAGE,
|
|
SEND_REACTION,
|
|
SET_FOCUSED_TAB
|
|
} from './actionTypes';
|
|
import {
|
|
addMessage,
|
|
addMessageReaction,
|
|
clearMessages,
|
|
closeChat,
|
|
notifyPrivateRecipientsChanged,
|
|
openChat,
|
|
setPrivateMessageRecipient
|
|
} from './actions';
|
|
import { ChatPrivacyDialog } from './components';
|
|
import {
|
|
ChatTabs,
|
|
INCOMING_MSG_SOUND_ID,
|
|
LOBBY_CHAT_MESSAGE,
|
|
MESSAGE_TYPE_ERROR,
|
|
MESSAGE_TYPE_LOCAL,
|
|
MESSAGE_TYPE_REMOTE,
|
|
MESSAGE_TYPE_SYSTEM
|
|
} from './constants';
|
|
import {
|
|
getDisplayNameSuffix,
|
|
getFocusedTab,
|
|
getUnreadCount,
|
|
isChatDisabled,
|
|
isSendGroupChatDisabled,
|
|
isVisitorChatParticipant
|
|
} from './functions';
|
|
import { INCOMING_MSG_SOUND_FILE } from './sounds';
|
|
import './subscriber';
|
|
|
|
/**
|
|
* Timeout for when to show the privacy notice after a private message was received.
|
|
*
|
|
* E.g. If this value is 20 secs (20000ms), then we show the privacy notice when sending a non private
|
|
* message after we have received a private message in the last 20 seconds.
|
|
*/
|
|
const PRIVACY_NOTICE_TIMEOUT = 20 * 1000;
|
|
|
|
/**
|
|
* Implements the middleware of the chat feature.
|
|
*
|
|
* @param {Store} store - The redux store.
|
|
* @returns {Function}
|
|
*/
|
|
MiddlewareRegistry.register(store => next => action => {
|
|
const { dispatch, getState } = store;
|
|
const localParticipant = getLocalParticipant(getState());
|
|
let isOpen, unreadCount;
|
|
|
|
switch (action.type) {
|
|
case ADD_MESSAGE:
|
|
unreadCount = getUnreadCount(getState());
|
|
if (action.isReaction) {
|
|
action.hasRead = false;
|
|
} else {
|
|
unreadCount = action.hasRead ? 0 : unreadCount + 1;
|
|
}
|
|
isOpen = getState()['features/chat'].isOpen;
|
|
|
|
if (typeof APP !== 'undefined') {
|
|
APP.API.notifyChatUpdated(unreadCount, isOpen);
|
|
}
|
|
break;
|
|
|
|
case APP_WILL_MOUNT:
|
|
dispatch(
|
|
registerSound(INCOMING_MSG_SOUND_ID, INCOMING_MSG_SOUND_FILE));
|
|
break;
|
|
|
|
case APP_WILL_UNMOUNT:
|
|
dispatch(unregisterSound(INCOMING_MSG_SOUND_ID));
|
|
break;
|
|
|
|
case CONFERENCE_JOINED:
|
|
_addChatMsgListener(action.conference, store);
|
|
break;
|
|
|
|
case CLOSE_CHAT: {
|
|
const { focusedTab } = getState()['features/chat'];
|
|
|
|
if (focusedTab === ChatTabs.CHAT) {
|
|
unreadCount = 0;
|
|
|
|
if (typeof APP !== 'undefined') {
|
|
APP.API.notifyChatUpdated(unreadCount, false);
|
|
}
|
|
} else if (focusedTab === ChatTabs.POLLS) {
|
|
dispatch(resetUnreadPollsCount());
|
|
}
|
|
break;
|
|
}
|
|
|
|
case ENDPOINT_MESSAGE_RECEIVED: {
|
|
const state = store.getState();
|
|
|
|
if (!isReactionsEnabled(state)) {
|
|
return next(action);
|
|
}
|
|
|
|
const { participant, data } = action;
|
|
|
|
if (data?.name === ENDPOINT_REACTION_NAME) {
|
|
// Skip duplicates, keep just 3.
|
|
const reactions = Array.from(new Set(data.reactions)).slice(0, 3) as string[];
|
|
|
|
store.dispatch(pushReactions(reactions));
|
|
|
|
_handleReceivedMessage(store, {
|
|
participantId: participant.getId(),
|
|
message: getReactionMessageFromBuffer(reactions),
|
|
privateMessage: false,
|
|
lobbyChat: false,
|
|
timestamp: data.timestamp
|
|
}, false, true);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case NON_PARTICIPANT_MESSAGE_RECEIVED: {
|
|
const { participantId, json: data } = action;
|
|
|
|
if (data?.type === MESSAGE_TYPE_SYSTEM && data.message) {
|
|
_handleReceivedMessage(store, {
|
|
displayName: data.displayName ?? i18next.t('chat.systemDisplayName'),
|
|
participantId,
|
|
lobbyChat: false,
|
|
message: data.message,
|
|
privateMessage: true,
|
|
timestamp: Date.now()
|
|
});
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case SET_FOCUSED_TAB:
|
|
case OPEN_CHAT: {
|
|
const state = store.getState();
|
|
const focusedTab = action.tabId || getFocusedTab(state);
|
|
|
|
if (focusedTab === ChatTabs.CHAT) {
|
|
// Don't allow opening chat if it's disabled AND user is trying to open the CHAT tab.
|
|
if (isChatDisabled(state)) {
|
|
return next(action);
|
|
}
|
|
unreadCount = 0;
|
|
|
|
if (typeof APP !== 'undefined') {
|
|
APP.API.notifyChatUpdated(unreadCount, true);
|
|
}
|
|
|
|
const { privateMessageRecipient } = state['features/chat'];
|
|
|
|
if (
|
|
isSendGroupChatDisabled(state)
|
|
&& privateMessageRecipient
|
|
&& !action.participant
|
|
) {
|
|
const participant = getParticipantById(state, privateMessageRecipient.id);
|
|
|
|
if (participant) {
|
|
action.participant = participant;
|
|
} else if (isVisitorChatParticipant(privateMessageRecipient)) {
|
|
// Handle visitor participants that don't exist in the main participant list
|
|
action.participant = privateMessageRecipient;
|
|
}
|
|
}
|
|
} else if (focusedTab === ChatTabs.POLLS) {
|
|
// Don't allow opening chat panel if polls are disabled AND user is trying to open the POLLS tab.
|
|
if (arePollsDisabled(state)) {
|
|
return next(action);
|
|
}
|
|
dispatch(resetUnreadPollsCount());
|
|
|
|
// Don't allow opening chat panel if file sharing is disabled AND user is trying to open the
|
|
// FILE_SHARING tab.
|
|
} else if (focusedTab === ChatTabs.FILE_SHARING && !isFileSharingEnabled(state)) {
|
|
return next(action);
|
|
|
|
// Don't allow opening chat panel if closed captions are disabled AND user is trying to open the
|
|
// CLOSED_CAPTIONS tab.
|
|
} else if (focusedTab === ChatTabs.CLOSED_CAPTIONS && !isCCTabEnabled(state)) {
|
|
return next(action);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case PARTICIPANT_JOINED:
|
|
case PARTICIPANT_LEFT:
|
|
case PARTICIPANT_UPDATED: {
|
|
if (_shouldNotifyPrivateRecipientsChanged(store, action)) {
|
|
const result = next(action);
|
|
|
|
dispatch(notifyPrivateRecipientsChanged());
|
|
|
|
return result;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case SEND_MESSAGE: {
|
|
const state = store.getState();
|
|
const conference = getCurrentConference(state);
|
|
|
|
if (conference) {
|
|
// There may be cases when we intend to send a private message but we forget to set the
|
|
// recipient. This logic tries to mitigate this risk.
|
|
const shouldSendPrivateMessageTo = _shouldSendPrivateMessageTo(state, action);
|
|
|
|
if (shouldSendPrivateMessageTo) {
|
|
const participantExists = getParticipantById(state, shouldSendPrivateMessageTo.id);
|
|
|
|
if (participantExists || shouldSendPrivateMessageTo.isFromVisitor) {
|
|
dispatch(openDialog(ChatPrivacyDialog, {
|
|
message: action.message,
|
|
participantID: shouldSendPrivateMessageTo.id,
|
|
isFromVisitor: shouldSendPrivateMessageTo.isFromVisitor,
|
|
displayName: shouldSendPrivateMessageTo.name
|
|
}));
|
|
}
|
|
} else {
|
|
// Sending the message if privacy notice doesn't need to be shown.
|
|
|
|
const { privateMessageRecipient, isLobbyChatActive, lobbyMessageRecipient }
|
|
= state['features/chat'];
|
|
|
|
if (typeof APP !== 'undefined') {
|
|
APP.API.notifySendingChatMessage(action.message, Boolean(privateMessageRecipient));
|
|
}
|
|
|
|
if (isLobbyChatActive && lobbyMessageRecipient) {
|
|
conference.sendLobbyMessage({
|
|
type: LOBBY_CHAT_MESSAGE,
|
|
message: action.message
|
|
}, lobbyMessageRecipient.id);
|
|
_persistSentPrivateMessage(store, lobbyMessageRecipient, action.message, true);
|
|
} else if (privateMessageRecipient) {
|
|
conference.sendPrivateTextMessage(privateMessageRecipient.id, action.message, 'body', isVisitorChatParticipant(privateMessageRecipient));
|
|
_persistSentPrivateMessage(store, privateMessageRecipient, action.message);
|
|
} else {
|
|
conference.sendTextMessage(action.message);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case SEND_REACTION: {
|
|
const state = store.getState();
|
|
const conference = getCurrentConference(state);
|
|
|
|
if (conference) {
|
|
const { reaction, messageId, receiverId } = action;
|
|
|
|
conference.sendReaction(reaction, messageId, receiverId);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case ADD_REACTION_MESSAGE: {
|
|
if (localParticipant?.id) {
|
|
_handleReceivedMessage(store, {
|
|
participantId: localParticipant.id,
|
|
message: action.message,
|
|
privateMessage: false,
|
|
timestamp: Date.now(),
|
|
lobbyChat: false
|
|
}, false, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
return next(action);
|
|
});
|
|
|
|
/**
|
|
* Set up state change listener to perform maintenance tasks when the conference
|
|
* is left or failed, e.g. Clear messages or close the chat modal if it's left
|
|
* open.
|
|
*/
|
|
StateListenerRegistry.register(
|
|
state => getCurrentConference(state),
|
|
(conference, { dispatch, getState }, previousConference) => {
|
|
if (conference !== previousConference) {
|
|
// conference changed, left or failed...
|
|
|
|
if (getState()['features/chat'].isOpen) {
|
|
// Closes the chat if it's left open.
|
|
dispatch(closeChat());
|
|
}
|
|
|
|
// Clear chat messages.
|
|
dispatch(clearMessages());
|
|
}
|
|
});
|
|
|
|
StateListenerRegistry.register(
|
|
state => state['features/chat'].isOpen,
|
|
(isOpen, { dispatch }) => {
|
|
if (typeof APP !== 'undefined' && isOpen) {
|
|
dispatch(showToolbox());
|
|
}
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Checks whether a notification for private chat recipients is needed.
|
|
*
|
|
* @param {IStore} store - The redux store.
|
|
* @param {{ participant: IParticipant, type: string }} action - The action.
|
|
* @returns {boolean}
|
|
*/
|
|
function _shouldNotifyPrivateRecipientsChanged(
|
|
store: IStore, action: { participant: IParticipant; type: string; }
|
|
) {
|
|
const { type, participant } = action;
|
|
|
|
if ([ PARTICIPANT_LEFT, PARTICIPANT_JOINED ].includes(type)) {
|
|
return true;
|
|
}
|
|
|
|
const { id, name } = participant;
|
|
|
|
return name !== getParticipantDisplayName(store, id);
|
|
}
|
|
|
|
/**
|
|
* Registers listener for {@link JitsiConferenceEvents.MESSAGE_RECEIVED} that
|
|
* will perform various chat related activities.
|
|
*
|
|
* @param {JitsiConference} conference - The conference instance on which the
|
|
* new event listener will be registered.
|
|
* @param {Object} store - The redux store object.
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
|
|
if (store.getState()['features/base/config'].iAmRecorder) {
|
|
// We don't register anything on web if we are in iAmRecorder mode
|
|
return;
|
|
}
|
|
|
|
conference.on(
|
|
JitsiConferenceEvents.MESSAGE_RECEIVED,
|
|
/* eslint-disable max-params */
|
|
(participantId: string, message: string, timestamp: number,
|
|
displayName: string, isFromVisitor: boolean, messageId: string, source: string) => {
|
|
/* eslint-enable max-params */
|
|
_onConferenceMessageReceived(store, {
|
|
// in case of messages coming from visitors we can have unknown id
|
|
participantId: participantId || displayName,
|
|
message,
|
|
timestamp,
|
|
displayName,
|
|
isFromVisitor,
|
|
messageId,
|
|
source,
|
|
privateMessage: false
|
|
});
|
|
|
|
if (isSendGroupChatDisabled(store.getState()) && participantId) {
|
|
const participant = getParticipantById(store, participantId);
|
|
|
|
store.dispatch(setPrivateMessageRecipient(participant));
|
|
}
|
|
}
|
|
);
|
|
|
|
conference.on(
|
|
JitsiConferenceEvents.REACTION_RECEIVED,
|
|
(participantId: string, reactionList: string[], messageId: string) => {
|
|
_onReactionReceived(store, {
|
|
participantId,
|
|
reactionList,
|
|
messageId
|
|
});
|
|
}
|
|
);
|
|
|
|
conference.on(
|
|
JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED,
|
|
(participantId: string, message: string, timestamp: number, messageId: string, displayName?: string, isFromVisitor?: boolean) => {
|
|
_onConferenceMessageReceived(store, {
|
|
participantId,
|
|
message,
|
|
timestamp,
|
|
displayName,
|
|
messageId,
|
|
privateMessage: true,
|
|
isFromVisitor
|
|
});
|
|
}
|
|
);
|
|
|
|
conference.on(
|
|
JitsiConferenceEvents.CONFERENCE_ERROR, (errorType: string, error: Error) => {
|
|
errorType === JitsiConferenceErrors.CHAT_ERROR && _handleChatError(store, error);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handles a received message.
|
|
*
|
|
* @param {Object} store - Redux store.
|
|
* @param {Object} message - The message object.
|
|
* @returns {void}
|
|
*/
|
|
function _onConferenceMessageReceived(store: IStore,
|
|
{ displayName, isFromVisitor, message, messageId, participantId, privateMessage, timestamp, source }: {
|
|
displayName?: string; isFromVisitor?: boolean; message: string; messageId?: string;
|
|
participantId: string; privateMessage: boolean; source?: string; timestamp: number; }
|
|
) {
|
|
|
|
const isGif = isGifEnabled(store.getState()) && isGifMessage(message);
|
|
|
|
if (isGif) {
|
|
_handleGifMessageReceived(store, participantId, message);
|
|
if (getGifDisplayMode(store.getState()) === 'tile') {
|
|
return;
|
|
}
|
|
}
|
|
_handleReceivedMessage(store, {
|
|
displayName,
|
|
isFromVisitor,
|
|
participantId,
|
|
message,
|
|
privateMessage,
|
|
lobbyChat: false,
|
|
timestamp,
|
|
messageId,
|
|
source
|
|
}, true, isGif);
|
|
}
|
|
|
|
/**
|
|
* Handles a received reaction.
|
|
*
|
|
* @param {Object} store - Redux store.
|
|
* @param {string} participantId - Id of the participant that sent the message.
|
|
* @param {string} reactionList - The list of received reactions.
|
|
* @param {string} messageId - The id of the message that the reaction is for.
|
|
* @returns {void}
|
|
*/
|
|
function _onReactionReceived(store: IStore, { participantId, reactionList, messageId }: {
|
|
messageId: string; participantId: string; reactionList: string[]; }) {
|
|
|
|
const reactionPayload = {
|
|
participantId,
|
|
reactionList,
|
|
messageId
|
|
};
|
|
|
|
store.dispatch(addMessageReaction(reactionPayload));
|
|
}
|
|
|
|
/**
|
|
* Handles a received gif message.
|
|
*
|
|
* @param {Object} store - Redux store.
|
|
* @param {string} participantId - Id of the participant that sent the message.
|
|
* @param {string} message - The message sent.
|
|
* @returns {void}
|
|
*/
|
|
function _handleGifMessageReceived(store: IStore, participantId: string, message: string) {
|
|
const url = extractGifURL(message);
|
|
|
|
store.dispatch(addGif(participantId, url));
|
|
}
|
|
|
|
/**
|
|
* Handles a chat error received from the xmpp server.
|
|
*
|
|
* @param {Store} store - The Redux store.
|
|
* @param {string} error - The error message.
|
|
* @returns {void}
|
|
*/
|
|
function _handleChatError({ dispatch }: IStore, error: Error) {
|
|
dispatch(addMessage({
|
|
hasRead: true,
|
|
messageType: MESSAGE_TYPE_ERROR,
|
|
message: error,
|
|
privateMessage: false,
|
|
timestamp: Date.now()
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Function to handle an incoming chat message from lobby room.
|
|
*
|
|
* @param {string} message - The message received.
|
|
* @param {string} participantId - The participant id.
|
|
* @returns {Function}
|
|
*/
|
|
export function handleLobbyMessageReceived(message: string, participantId: string) {
|
|
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
|
_handleReceivedMessage({ dispatch,
|
|
getState }, { participantId,
|
|
message,
|
|
privateMessage: false,
|
|
lobbyChat: true,
|
|
timestamp: Date.now() });
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
* Function to get lobby chat user display name.
|
|
*
|
|
* @param {Store} state - The Redux store.
|
|
* @param {string} participantId - The knocking participant id.
|
|
* @returns {string}
|
|
*/
|
|
function getLobbyChatDisplayName(state: IReduxState, participantId: string) {
|
|
const { knockingParticipants } = state['features/lobby'];
|
|
const { lobbyMessageRecipient } = state['features/chat'];
|
|
|
|
if (participantId === lobbyMessageRecipient?.id) {
|
|
return lobbyMessageRecipient.name;
|
|
}
|
|
|
|
const knockingParticipant = knockingParticipants.find(p => p.id === participantId);
|
|
|
|
if (knockingParticipant) {
|
|
return knockingParticipant.name;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Function to handle an incoming chat message.
|
|
*
|
|
* @param {Store} store - The Redux store.
|
|
* @param {Object} message - The message object.
|
|
* @param {boolean} shouldPlaySound - Whether to play the incoming message sound.
|
|
* @param {boolean} isReaction - Whether the message is a reaction message.
|
|
* @returns {void}
|
|
*/
|
|
function _handleReceivedMessage({ dispatch, getState }: IStore,
|
|
{ displayName, isFromVisitor, lobbyChat, message, messageId, participantId, privateMessage, source, timestamp }: {
|
|
displayName?: string; isFromVisitor?: boolean; lobbyChat: boolean; message: string;
|
|
messageId?: string; participantId: string; privateMessage: boolean; source?: string; timestamp: number; },
|
|
shouldPlaySound = true,
|
|
isReaction = false
|
|
) {
|
|
// Logic for all platforms:
|
|
const state = getState();
|
|
const { isOpen: isChatOpen } = state['features/chat'];
|
|
const { soundsIncomingMessage: soundEnabled, userSelectedNotifications } = state['features/base/settings'];
|
|
|
|
// Don't play sound or show notifications if chat is disabled.
|
|
if (isChatDisabled(state)) {
|
|
return;
|
|
}
|
|
|
|
if (soundEnabled && shouldPlaySound && !isChatOpen) {
|
|
dispatch(playSound(INCOMING_MSG_SOUND_ID));
|
|
}
|
|
|
|
const participant = getParticipantById(state, participantId);
|
|
|
|
const localParticipant = getLocalParticipant(getState);
|
|
let _displayName;
|
|
|
|
if (lobbyChat) {
|
|
_displayName = getLobbyChatDisplayName(state, participantId);
|
|
} else if (isFromVisitor) {
|
|
_displayName = getDisplayName(state, displayName);
|
|
} else if (!participant) {
|
|
_displayName = getDisplayName(state, displayName);
|
|
} else {
|
|
_displayName = getParticipantDisplayName(state, participantId);
|
|
}
|
|
|
|
const hasRead = participant?.local || isChatOpen;
|
|
const timestampToDate = timestamp ? new Date(timestamp) : new Date();
|
|
const millisecondsTimestamp = timestampToDate.getTime();
|
|
|
|
// skip message notifications on join (the messages having timestamp - coming from the history)
|
|
const shouldShowNotification = userSelectedNotifications?.['notify.chatMessages']
|
|
&& !hasRead && !isReaction && (!timestamp || lobbyChat);
|
|
const newMessage = {
|
|
displayName: _displayName,
|
|
hasRead,
|
|
participantId,
|
|
messageType: participant?.local ? MESSAGE_TYPE_LOCAL : MESSAGE_TYPE_REMOTE,
|
|
message,
|
|
privateMessage,
|
|
lobbyChat,
|
|
recipient: getParticipantDisplayName(state, localParticipant?.id ?? ''),
|
|
timestamp: millisecondsTimestamp,
|
|
messageId,
|
|
isReaction,
|
|
isFromVisitor,
|
|
isFromGuest: source === 'guest'
|
|
};
|
|
|
|
dispatch(addMessage(newMessage));
|
|
|
|
let notificationDisplayName = _displayName;
|
|
|
|
// source can be 'token' or 'guest'. When it is 'guest', we append a guest indicator
|
|
// to the display name to notify users that this message can be anything, while the token one is coming
|
|
// from a trusted source (the jwt token, the name can be locked from the backend if the
|
|
// token has the feature 'name-readonly')
|
|
if (isFromVisitor || (!participant && source === 'guest')) {
|
|
// @ts-ignore
|
|
notificationDisplayName = `${_displayName} ${getDisplayNameSuffix(newMessage)}`;
|
|
}
|
|
|
|
if (shouldShowNotification) {
|
|
dispatch(showMessageNotification({
|
|
title: notificationDisplayName,
|
|
description: message,
|
|
customActionNameKey: [ 'notify.openChat' ],
|
|
customActionHandler: [ () => dispatch(openChat()) ]
|
|
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
|
}
|
|
|
|
if (typeof APP !== 'undefined') {
|
|
// Logic for web only:
|
|
|
|
APP.API.notifyReceivedChatMessage({
|
|
body: message,
|
|
from: participantId,
|
|
nick: notificationDisplayName,
|
|
privateMessage,
|
|
ts: timestamp
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Interface for recipient objects used in private messaging.
|
|
*/
|
|
interface IRecipient {
|
|
id: string;
|
|
isVisitor?: boolean;
|
|
name?: string;
|
|
}
|
|
|
|
/**
|
|
* Persists the sent private messages as if they were received over the muc.
|
|
*
|
|
* This is required as we rely on the fact that we receive all messages from the muc that we send
|
|
* (as they are sent to everybody), but we don't receive the private messages we send to another participant.
|
|
* But those messages should be in the store as well, otherwise they don't appear in the chat window.
|
|
*
|
|
* @param {Store} store - The Redux store.
|
|
* @param {IRecipient} recipient - The recipient the private message was sent to.
|
|
* @param {string} message - The sent message.
|
|
* @param {boolean} isLobbyPrivateMessage - Is a lobby message.
|
|
* @returns {void}
|
|
*/
|
|
function _persistSentPrivateMessage({ dispatch, getState }: IStore, recipient: IRecipient,
|
|
message: string, isLobbyPrivateMessage = false) {
|
|
const state = getState();
|
|
const localParticipant = getLocalParticipant(state);
|
|
|
|
if (!localParticipant?.id) {
|
|
return;
|
|
}
|
|
const displayName = getParticipantDisplayName(state, localParticipant.id);
|
|
const { lobbyMessageRecipient } = state['features/chat'];
|
|
|
|
const recipientName
|
|
= recipient.isVisitor
|
|
? getDisplayName(state, recipient.name)
|
|
: (isLobbyPrivateMessage
|
|
? lobbyMessageRecipient?.name
|
|
: getParticipantDisplayName(getState, recipient?.id));
|
|
|
|
dispatch(addMessage({
|
|
displayName,
|
|
hasRead: true,
|
|
participantId: localParticipant.id,
|
|
messageType: MESSAGE_TYPE_LOCAL,
|
|
message,
|
|
privateMessage: !isLobbyPrivateMessage,
|
|
lobbyChat: isLobbyPrivateMessage,
|
|
recipient: recipientName,
|
|
sentToVisitor: recipient.isVisitor,
|
|
timestamp: Date.now()
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Returns the participant info for who we may have wanted to send the message
|
|
* that we're about to send.
|
|
*
|
|
* @param {Object} state - The Redux state.
|
|
* @param {Object} action - The action being dispatched now.
|
|
* @returns {IRecipient?} - The recipient info or undefined if no notice should be shown.
|
|
*/
|
|
function _shouldSendPrivateMessageTo(state: IReduxState, action: AnyAction) {
|
|
if (action.ignorePrivacy) {
|
|
// Shortcut: this is only true, if we already displayed the notice, so no need to show it again.
|
|
return undefined;
|
|
}
|
|
|
|
const { messages, privateMessageRecipient } = state['features/chat'];
|
|
|
|
if (privateMessageRecipient) {
|
|
// We're already sending a private message, no need to warn about privacy.
|
|
return undefined;
|
|
}
|
|
|
|
if (!messages.length) {
|
|
// No messages yet, no need to warn for privacy.
|
|
return undefined;
|
|
}
|
|
|
|
// Platforms sort messages differently
|
|
const lastMessage = navigator.product === 'ReactNative'
|
|
? messages[0] : messages[messages.length - 1];
|
|
|
|
if (lastMessage.messageType === MESSAGE_TYPE_LOCAL) {
|
|
// The sender is probably aware of any private messages as already sent
|
|
// a message since then. Doesn't make sense to display the notice now.
|
|
return undefined;
|
|
}
|
|
|
|
if (lastMessage.privateMessage) {
|
|
if (!lastMessage.participantId) {
|
|
// this is a system message we can ignore
|
|
return undefined;
|
|
}
|
|
|
|
// We show the notice if the last received message was private.
|
|
return {
|
|
id: lastMessage.participantId,
|
|
isFromVisitor: Boolean(lastMessage.isFromVisitor),
|
|
name: lastMessage.displayName
|
|
};
|
|
}
|
|
|
|
// But messages may come rapidly, we want to protect our users from mis-sending a message
|
|
// even when there was a reasonable recently received private message.
|
|
const now = Date.now();
|
|
const recentPrivateMessages = messages.filter(
|
|
message =>
|
|
message.messageType !== MESSAGE_TYPE_LOCAL
|
|
&& message.privateMessage
|
|
&& message.timestamp + PRIVACY_NOTICE_TIMEOUT > now);
|
|
const recentPrivateMessage = navigator.product === 'ReactNative'
|
|
? recentPrivateMessages[0] : recentPrivateMessages[recentPrivateMessages.length - 1];
|
|
|
|
if (recentPrivateMessage) {
|
|
return {
|
|
id: recentPrivateMessage.participantId,
|
|
isFromVisitor: Boolean(recentPrivateMessage.isFromVisitor),
|
|
name: recentPrivateMessage.displayName
|
|
};
|
|
}
|
|
|
|
return undefined;
|
|
}
|