feat(file-sharing): show count badges for unread files and notify on uploads/removals (#16484)

This commit is contained in:
Mihaela Dumitru
2025-10-03 01:52:24 +03:00
parent 6bf41c4e49
commit 529d67d2bb
10 changed files with 141 additions and 22 deletions

View File

@@ -561,8 +561,12 @@
"downloadFile": "Download",
"dragAndDrop": "Drag and drop files here or anywhere on screen",
"fileAlreadyUploaded": "File has already been uploaded to this meeting.",
"fileRemovedByOther": "Your file '{{ fileName }}' was removed",
"fileTooLargeDescription": "Please make sure the file does not exceed {{ maxFileSize }}.",
"fileTooLargeTitle": "The selected file is too large",
"fileUploadProgress": "File upload progress",
"fileUploadedSuccessfully": "File uploaded successfully",
"newFileNotification": "{{ participantName }} shared '{{ fileName }}'",
"removeFile": "Remove",
"uploadFailedDescription": "Please try again.",
"uploadFailedTitle": "Upload failed",

View File

@@ -7,7 +7,9 @@ import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { TabBarLabelCounter } from '../../../mobile/navigation/components/TabBarLabelCounter';
import { getUnreadPollCount } from '../../../polls/functions';
import { closeChat, sendMessage } from '../../actions.native';
import { getUnreadFilesCount } from '../../functions';
import { IChatProps as AbstractProps } from '../../types';
import ChatInputBar from './ChatInputBar';
@@ -17,6 +19,21 @@ import styles from './styles';
interface IProps extends AbstractProps {
/**
* The number of unread file messages.
*/
_nbUnreadFiles: number;
/**
* The number of unread messages.
*/
_nbUnreadMessages: number;
/**
* The number of unread polls.
*/
_nbUnreadPolls: number;
/**
* Default prop for navigating between screen components(React Navigation).
*/
@@ -96,7 +113,9 @@ class Chat extends Component<IProps> {
* @private
* @returns {{
* _messages: Array<Object>,
* _nbUnreadMessages: number
* _nbUnreadMessages: number,
* _nbUnreadPolls: number,
* _nbUnreadFiles: number
* }}
*/
function _mapStateToProps(state: IReduxState, _ownProps: any) {
@@ -104,13 +123,16 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
return {
_messages: messages,
_nbUnreadMessages: nbUnreadMessages
_nbUnreadMessages: nbUnreadMessages,
_nbUnreadPolls: getUnreadPollCount(state),
_nbUnreadFiles: getUnreadFilesCount(state)
};
}
export default translate(connect(_mapStateToProps)((props: IProps) => {
const { _nbUnreadMessages, dispatch, navigation, t } = props;
const unreadMessagesNr = _nbUnreadMessages > 0;
const { _nbUnreadMessages, _nbUnreadPolls, _nbUnreadFiles, dispatch, navigation, t } = props;
const totalUnread = _nbUnreadMessages + _nbUnreadPolls + _nbUnreadFiles;
const unreadMessagesNr = totalUnread > 0;
const isFocused = useIsFocused();
@@ -121,14 +143,14 @@ export default translate(connect(_mapStateToProps)((props: IProps) => {
activeUnreadNr = { unreadMessagesNr }
isFocused = { isFocused }
label = { t('chat.tabs.chat') }
nbUnread = { _nbUnreadMessages } />
nbUnread = { totalUnread } />
)
});
return () => {
isFocused && dispatch(closeChat());
};
}, [ isFocused, _nbUnreadMessages ]);
}, [ isFocused, _nbUnreadMessages, _nbUnreadPolls, _nbUnreadFiles ]);
return (
<Chat { ...props } />

View File

@@ -10,7 +10,7 @@ import { arePollsDisabled } from '../../../conference/functions.any';
import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import { getUnreadPollCount } from '../../../polls/functions';
import { getUnreadCount } from '../../functions';
import { getUnreadCount, getUnreadFilesCount } from '../../functions';
interface IProps extends AbstractButtonProps {
@@ -70,9 +70,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
return {
_isPollsDisabled: arePollsDisabled(state),
// The toggled icon should also be available for new polls
_unreadMessageCount: getUnreadCount(state) || getUnreadPollCount(state),
_unreadMessageCount: getUnreadCount(state) || getUnreadPollCount(state) || getUnreadFilesCount(state),
visible
};
}

View File

@@ -64,6 +64,11 @@ interface IProps extends AbstractProps {
*/
_isResizing: boolean;
/**
* Number of unread file sharing messages.
*/
_nbUnreadFiles: number;
/**
* Number of unread poll messages.
*/
@@ -229,6 +234,7 @@ const Chat = ({
_focusedTab,
_isResizing,
_messages,
_nbUnreadFiles,
_nbUnreadMessages,
_nbUnreadPolls,
_onSendMessage,
@@ -481,7 +487,7 @@ const Chat = ({
if (_isFileSharingTabEnabled) {
tabs.push({
accessibilityLabel: t('chat.tabs.fileSharing'),
countBadge: undefined,
countBadge: _focusedTab !== ChatTabs.FILE_SHARING && _nbUnreadFiles > 0 ? _nbUnreadFiles : undefined,
id: ChatTabs.FILE_SHARING,
controlsId: `${ChatTabs.FILE_SHARING}-panel`,
icon: IconShareDoc
@@ -554,13 +560,14 @@ const Chat = ({
* _messages: Array<Object>,
* _nbUnreadMessages: number,
* _nbUnreadPolls: number,
* _nbUnreadFiles: number,
* _showNamePrompt: boolean,
* _width: number,
* _isResizing: boolean
* }}
*/
function _mapStateToProps(state: IReduxState, _ownProps: any) {
const { isOpen, focusedTab, messages, nbUnreadMessages, width, isResizing } = state['features/chat'];
const { isOpen, focusedTab, messages, nbUnreadMessages, nbUnreadFiles, width, isResizing } = state['features/chat'];
const { nbUnreadPolls } = state['features/polls'];
const _localParticipant = getLocalParticipant(state);
@@ -574,6 +581,7 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
_messages: messages,
_nbUnreadMessages: nbUnreadMessages,
_nbUnreadPolls: nbUnreadPolls,
_nbUnreadFiles: nbUnreadFiles,
_showNamePrompt: !_localParticipant?.name,
_width: width?.current || CHAT_SIZE,
_isResizing: isResizing

View File

@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { getUnreadPollCount } from '../../../polls/functions';
import { getUnreadCount } from '../../functions';
import { getUnreadCount, getUnreadFilesCount } from '../../functions';
/**
* The type of the React {@code Component} props of {@link ChatCounter}.
@@ -65,7 +65,7 @@ function _mapStateToProps(state: IReduxState) {
return {
_count: getUnreadCount(state) + getUnreadPollCount(state),
_count: getUnreadCount(state) + getUnreadPollCount(state) + getUnreadFilesCount(state),
_isOpen: isOpen
};

View File

@@ -131,6 +131,16 @@ export function getUnreadCount(state: IReduxState) {
return messagesCount - (lastReadIndex + 1) - reactionMessages;
}
/**
* Gets the unread files count.
*
* @param {IReduxState} state - The redux state.
* @returns {number} The number of unread files.
*/
export function getUnreadFilesCount(state: IReduxState): number {
return state['features/chat']?.nbUnreadFiles || 0;
}
/**
* Get whether the chat smileys are disabled or not.
*

View File

@@ -1,6 +1,7 @@
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 {
@@ -28,6 +29,7 @@ const DEFAULT_STATE = {
messages: [],
reactions: {},
nbUnreadMessages: 0,
nbUnreadFiles: 0,
privateMessageRecipient: undefined,
lobbyMessageRecipient: undefined,
isLobbyChatActive: false,
@@ -51,6 +53,7 @@ export interface IChatState {
name: string;
} | ILocalParticipant;
messages: IMessage[];
nbUnreadFiles: number;
nbUnreadMessages: number;
privateMessageRecipient?: IParticipant | IVisitorChatParticipant;
width: {
@@ -231,7 +234,8 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
return {
...state,
focusedTab: action.tabId,
nbUnreadMessages: action.tabId === ChatTabs.CHAT ? 0 : state.nbUnreadMessages
nbUnreadMessages: action.tabId === ChatTabs.CHAT ? 0 : state.nbUnreadMessages,
nbUnreadFiles: action.tabId === ChatTabs.FILE_SHARING ? 0 : state.nbUnreadFiles
};
case SET_CHAT_WIDTH: {
@@ -262,6 +266,23 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
isResizing: action.resizing
};
}
case ADD_FILE:
return {
...state,
nbUnreadFiles: action.shouldIncrementUnread ? state.nbUnreadFiles + 1 : state.nbUnreadFiles
};
case _FILE_LIST_RECEIVED: {
const remoteFilesCount = Object.values(action.files).filter(
(file: any) => file.authorParticipantId !== action.localParticipantId
).length;
return {
...state,
nbUnreadFiles: remoteFilesCount
};
}
}
return state;

View File

@@ -39,12 +39,14 @@ export function updateFileProgress(fileId: string, progress: number) {
* Add a file.
*
* @param {IFileMetadata} file - The file to add to the state.
* @param {boolean} shouldIncrementUnread - Whether to increment the unread count.
* @returns {Object}
*/
export function addFile(file: IFileMetadata) {
export function addFile(file: IFileMetadata, shouldIncrementUnread = false) {
return {
type: ADD_FILE,
file
file,
shouldIncrementUnread
};
}

View File

@@ -6,8 +6,10 @@ import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { getLocalParticipant, getParticipantDisplayName } from '../base/participants/functions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { showErrorNotification } from '../notifications/actions';
import { ChatTabs } from '../chat/constants';
import { showErrorNotification, showNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants';
import { I_AM_VISITOR_MODE } from '../visitors/actionTypes';
import { DOWNLOAD_FILE, REMOVE_FILE, UPLOAD_FILES, _FILE_LIST_RECEIVED, _FILE_REMOVED } from './actionTypes';
import { addFile, removeFile, updateFileProgress } from './actions';
@@ -23,12 +25,40 @@ import { downloadFile } from './utils';
*/
StateListenerRegistry.register(
state => state['features/base/conference'].conference,
(conference, { dispatch }, previousConference) => {
(conference, { dispatch, getState }, previousConference) => {
if (conference && !previousConference) {
conference.on(JitsiConferenceEvents.FILE_SHARING_FILE_ADDED, (file: IFileMetadata) => {
dispatch(addFile(file));
const state = getState();
const localParticipant = getLocalParticipant(state);
const isRemoteFile = file.authorParticipantId !== localParticipant?.id;
const { isOpen, focusedTab } = state['features/chat'];
const isFileSharingTabVisible = isOpen && focusedTab === ChatTabs.FILE_SHARING;
dispatch(addFile(file, isRemoteFile && !isFileSharingTabVisible));
if (isRemoteFile && !isFileSharingTabVisible) {
dispatch(showNotification({
titleKey: 'fileSharing.newFileNotification',
titleArguments: { participantName: file.authorParticipantName, fileName: file.fileName }
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
}
});
conference.on(JitsiConferenceEvents.FILE_SHARING_FILE_REMOVED, (fileId: string) => {
const state = getState();
const localParticipant = getLocalParticipant(state);
const { files } = state['features/file-sharing'];
const { isOpen, focusedTab } = state['features/chat'];
const removedFile = files.get(fileId);
const isFileSharingTabVisible = isOpen && focusedTab === ChatTabs.FILE_SHARING;
if (removedFile && removedFile.authorParticipantId === localParticipant?.id && !isFileSharingTabVisible) {
dispatch(showNotification({
titleKey: 'fileSharing.fileRemovedByOther',
titleArguments: { fileName: removedFile.fileName },
appearance: NOTIFICATION_TYPE.WARNING
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
}
dispatch({
type: _FILE_REMOVED,
fileId
@@ -36,9 +66,13 @@ StateListenerRegistry.register(
});
conference.on(JitsiConferenceEvents.FILE_SHARING_FILES_RECEIVED, (files: object) => {
const state = getState();
const localParticipant = getLocalParticipant(state);
dispatch({
type: _FILE_LIST_RECEIVED,
files
files,
localParticipantId: localParticipant?.id
});
});
}
@@ -52,6 +86,17 @@ StateListenerRegistry.register(
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case I_AM_VISITOR_MODE: {
if (!action.iAmVisitor) {
const state = store.getState();
const conference = getCurrentConference(state);
conference?.getFileSharing()?.requestFileList?.();
}
return next(action);
}
case UPLOAD_FILES: {
const state = store.getState();
const conference = getCurrentConference(state);

View File

@@ -1,6 +1,11 @@
import ReducerRegistry from '../base/redux/ReducerRegistry';
import { ADD_FILE, UPDATE_FILE_UPLOAD_PROGRESS, _FILE_LIST_RECEIVED, _FILE_REMOVED } from './actionTypes';
import {
ADD_FILE,
UPDATE_FILE_UPLOAD_PROGRESS,
_FILE_LIST_RECEIVED,
_FILE_REMOVED
} from './actionTypes';
import { IFileMetadata } from './types';
export interface IFileSharingState {
@@ -20,6 +25,7 @@ ReducerRegistry.register<IFileSharingState>('features/file-sharing',
newFiles.set(action.file.fileId, action.file);
return {
...state,
files: newFiles
};
}
@@ -30,6 +36,7 @@ ReducerRegistry.register<IFileSharingState>('features/file-sharing',
newFiles.delete(action.fileId);
return {
...state,
files: newFiles
};
}
@@ -43,12 +50,14 @@ ReducerRegistry.register<IFileSharingState>('features/file-sharing',
}
return {
...state,
files: newFiles
};
}
case _FILE_LIST_RECEIVED: {
return {
...state,
files: new Map(Object.entries(action.files))
};
}