From fae82a237c29e837dba8fa81f3175c6a6985e447 Mon Sep 17 00:00:00 2001 From: martincupela Date: Tue, 3 Jun 2025 12:15:11 +0200 Subject: [PATCH 01/11] feat: add message reminders --- .../Channel/__tests__/Channel.test.js | 2 +- src/components/Chat/hooks/useChat.ts | 4 + src/components/Dialog/ButtonWithSubmenu.tsx | 137 ++++++++++++++++++ src/components/Dialog/index.ts | 1 + src/components/Message/Message.tsx | 32 ++-- src/components/Message/MessageSimple.tsx | 4 + .../Message/ReminderNotification.tsx | 34 +++++ src/components/Message/hooks/index.ts | 1 + .../Message/hooks/useMessageReminder.ts | 16 ++ src/components/Message/utils.tsx | 18 +++ .../MessageActions/MessageActionsBox.tsx | 34 ++++- .../MessageActions/RemindMeSubmenu.tsx | 51 +++++++ src/experimental/MessageActions/defaults.tsx | 50 ++++++- src/i18n/Streami18n.ts | 2 + src/i18n/de.json | 7 + src/i18n/en.json | 8 + src/i18n/es.json | 7 + src/i18n/fr.json | 7 + src/i18n/hi.json | 7 + src/i18n/it.json | 7 + src/i18n/ja.json | 7 + src/i18n/ko.json | 7 + src/i18n/nl.json | 7 + src/i18n/pt.json | 7 + src/i18n/ru.json | 7 + src/i18n/tr.json | 7 + src/i18n/types.ts | 55 +++++++ src/i18n/utils.ts | 14 +- 28 files changed, 511 insertions(+), 29 deletions(-) create mode 100644 src/components/Dialog/ButtonWithSubmenu.tsx create mode 100644 src/components/Message/ReminderNotification.tsx create mode 100644 src/components/Message/hooks/useMessageReminder.ts create mode 100644 src/components/MessageActions/RemindMeSubmenu.tsx diff --git a/src/components/Channel/__tests__/Channel.test.js b/src/components/Channel/__tests__/Channel.test.js index 341f51827c..74fbf5ed28 100644 --- a/src/components/Channel/__tests__/Channel.test.js +++ b/src/components/Channel/__tests__/Channel.test.js @@ -1509,7 +1509,7 @@ describe('Channel', () => { if (!hasSent) { const m = generateMessage({ id: messageId, - status: 'sending', // FIXME: had to have been explicitly added + status: 'sending', // FIXME: had to be explicitly added text: messageText, }); sendMessage({ localMessage: { ...m, status: 'sending' }, message: m }); diff --git a/src/components/Chat/hooks/useChat.ts b/src/components/Chat/hooks/useChat.ts index 21523ecdfc..d94897f756 100644 --- a/src/components/Chat/hooks/useChat.ts +++ b/src/components/Chat/hooks/useChat.ts @@ -66,10 +66,14 @@ export const useChat = ({ client.threads.registerSubscriptions(); client.polls.registerSubscriptions(); + client.reminders.registerSubscriptions(); + client.reminders.initTimers(); return () => { client.threads.unregisterSubscriptions(); client.polls.unregisterSubscriptions(); + client.reminders.unregisterSubscriptions(); + client.reminders.clearTimers(); }; }, [client]); diff --git a/src/components/Dialog/ButtonWithSubmenu.tsx b/src/components/Dialog/ButtonWithSubmenu.tsx new file mode 100644 index 0000000000..c8f2587a3d --- /dev/null +++ b/src/components/Dialog/ButtonWithSubmenu.tsx @@ -0,0 +1,137 @@ +import clsx from 'clsx'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useDialog, useDialogIsOpen } from './hooks'; +import { useDialogAnchor } from './DialogAnchor'; +import type { ComponentProps, ComponentType } from 'react'; +import type { Placement } from '@popperjs/core'; + +type ButtonWithSubmenu = ComponentProps<'button'> & { + children: React.ReactNode; + placement: Placement; + Submenu: ComponentType; + submenuContainerProps?: ComponentProps<'div'>; +}; +export const ButtonWithSubmenu = ({ + children, + className, + placement, + Submenu, + submenuContainerProps, + ...buttonProps +}: ButtonWithSubmenu) => { + const buttonRef = useRef(null); + const [dialogContainer, setDialogContainer] = useState(null); + const keepSubmenuOpen = useRef(false); + const dialogCloseTimeout = useRef(null); + const dialogId = useMemo(() => `submenu-${Math.random().toString(36).slice(2)}`, []); + const dialog = useDialog({ id: dialogId }); + const dialogIsOpen = useDialogIsOpen(dialogId); + const { attributes, setPopperElement, styles } = useDialogAnchor({ + open: dialogIsOpen, + placement, + referenceElement: buttonRef.current, + }); + + const closeDialogLazily = useCallback(() => { + if (dialogCloseTimeout.current) clearTimeout(dialogCloseTimeout.current); + dialogCloseTimeout.current = setTimeout(() => { + if (keepSubmenuOpen.current) return; + dialog.close(); + }, 100); + }, [dialog]); + + const handleClose = useCallback( + (event: Event) => { + const parentButton = buttonRef.current; + if (!dialogIsOpen || !parentButton) return; + event.stopPropagation(); + closeDialogLazily(); + parentButton.focus(); + }, + [closeDialogLazily, dialogIsOpen, buttonRef], + ); + + const handleFocusParentButton = () => { + if (dialogIsOpen) return; + dialog.open(); + keepSubmenuOpen.current = true; + }; + + useEffect(() => { + const parentButton = buttonRef.current; + if (!dialogIsOpen || !parentButton) return; + const hideOnEscape = (event: KeyboardEvent) => { + if (event.key !== 'Escape') return; + handleClose(event); + keepSubmenuOpen.current = false; + }; + + document.addEventListener('keyup', hideOnEscape, { capture: true }); + + return () => { + document.removeEventListener('keyup', hideOnEscape, { capture: true }); + }; + }, [dialogIsOpen, handleClose]); + + return ( + <> + + {dialogIsOpen && ( +
{ + const isBlurredDescendant = + event.relatedTarget instanceof Node && + dialogContainer?.contains(event.relatedTarget); + if (isBlurredDescendant) return; + keepSubmenuOpen.current = false; + closeDialogLazily(); + }} + onFocus={() => { + keepSubmenuOpen.current = true; + }} + onMouseEnter={() => { + keepSubmenuOpen.current = true; + }} + onMouseLeave={() => { + keepSubmenuOpen.current = false; + closeDialogLazily(); + }} + ref={(element) => { + setPopperElement(element); + setDialogContainer(element); + }} + style={styles.popper} + tabIndex={-1} + {...submenuContainerProps} + > + +
+ )} + + ); +}; diff --git a/src/components/Dialog/index.ts b/src/components/Dialog/index.ts index a2462dbcdd..7e3fd2bf08 100644 --- a/src/components/Dialog/index.ts +++ b/src/components/Dialog/index.ts @@ -1,3 +1,4 @@ +export * from './ButtonWithSubmenu'; export * from './DialogAnchor'; export * from './DialogManager'; export * from './DialogPortal'; diff --git a/src/components/Message/Message.tsx b/src/components/Message/Message.tsx index 26e6d9cd2a..7e1316f899 100644 --- a/src/components/Message/Message.tsx +++ b/src/components/Message/Message.tsx @@ -40,13 +40,13 @@ type MessagePropsToOmit = type MessageContextPropsToPick = | 'handleAction' | 'handleDelete' + | 'handleFetchReactions' | 'handleFlag' | 'handleMarkUnread' | 'handleMute' | 'handleOpenThread' | 'handlePin' | 'handleReaction' - | 'handleFetchReactions' | 'handleRetry' | 'mutes' | 'onMentionsClickMessage' @@ -74,7 +74,7 @@ const MessageWithContext = (props: MessageWithContextProps) => { } = props; const { client, isMessageAIGenerated } = useChatContext('Message'); - const { read } = useChannelStateContext('Message'); + const { channelConfig, read } = useChannelStateContext('Message'); const { Message: contextMessage } = useComponentContext('Message'); const actionsEnabled = message.type === 'regular' && message.status === 'received'; @@ -115,17 +115,22 @@ const MessageWithContext = (props: MessageWithContextProps) => { const messageActionsHandler = useCallback( () => - getMessageActions(messageActions, { - canDelete, - canEdit, - canFlag, - canMarkUnread, - canMute, - canPin, - canQuote, - canReact, - canReply, - }), + getMessageActions( + messageActions, + { + canDelete, + canEdit, + canFlag, + canMarkUnread, + canMute, + canPin, + canQuote, + canReact, + canReply, + }, + channelConfig, + ), + [ messageActions, canDelete, @@ -137,6 +142,7 @@ const MessageWithContext = (props: MessageWithContextProps) => { canQuote, canReact, canReply, + channelConfig, ], ); diff --git a/src/components/Message/MessageSimple.tsx b/src/components/Message/MessageSimple.tsx index 9b4c23b875..63a1f71a5e 100644 --- a/src/components/Message/MessageSimple.tsx +++ b/src/components/Message/MessageSimple.tsx @@ -37,6 +37,8 @@ import type { MessageUIComponentProps } from './types'; import { StreamedMessageText as DefaultStreamedMessageText } from './StreamedMessageText'; import { isDateSeparatorMessage } from '../MessageList'; +import { ReminderNotification } from './ReminderNotification'; +import { useMessageReminder } from './hooks'; type MessageSimpleWithContextProps = MessageContextValue; @@ -63,6 +65,7 @@ const MessageSimpleWithContext = (props: MessageSimpleWithContextProps) => { const { t } = useTranslationContext('MessageSimple'); const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false); const [isEditedTimestampOpen, setEditedTimestampOpen] = useState(false); + const reminder = useMessageReminder(message.id); const { Attachment = DefaultAttachment, @@ -155,6 +158,7 @@ const MessageSimpleWithContext = (props: MessageSimpleWithContextProps) => { {
{PinIndicator && } + {!!reminder && } {message.user && ( ({ + timeLeftMs: state.timeLeftMs, +}); +export const ReminderNotification = ({ reminder }: ReminderNotificationProps) => { + const { t } = useTranslationContext(); + const { timeLeftMs } = useStateStore(reminder?.state, reminderStateSelector) ?? {}; + + return ( +

+ {t('Saved for later')} + {timeLeftMs !== null && ( + <> + | + + {t(`Due {{ dueTimeElapsed }}`, { + dueTimeElapsed: t('duration/Message reminder', { + milliseconds: timeLeftMs, + }), + })} + + + )} +

+ ); +}; diff --git a/src/components/Message/hooks/index.ts b/src/components/Message/hooks/index.ts index 14e403552a..801c9d374b 100644 --- a/src/components/Message/hooks/index.ts +++ b/src/components/Message/hooks/index.ts @@ -13,3 +13,4 @@ export * from './useUserHandler'; export * from './useUserRole'; export * from './useReactionsFetcher'; export * from './useMessageTextStreaming'; +export * from './useMessageReminder'; diff --git a/src/components/Message/hooks/useMessageReminder.ts b/src/components/Message/hooks/useMessageReminder.ts new file mode 100644 index 0000000000..677ab3a7d9 --- /dev/null +++ b/src/components/Message/hooks/useMessageReminder.ts @@ -0,0 +1,16 @@ +import { useCallback } from 'react'; +import { useChatContext } from '../../../context'; +import { useStateStore } from '../../../store'; +import type { ReminderManagerState } from 'stream-chat'; + +export const useMessageReminder = (messageId: string) => { + const { client } = useChatContext(); + const reminderSelector = useCallback( + (state: ReminderManagerState) => ({ + reminder: state.reminders.get(messageId), + }), + [messageId], + ); + const { reminder } = useStateStore(client.reminders.state, reminderSelector); + return reminder; +}; diff --git a/src/components/Message/utils.tsx b/src/components/Message/utils.tsx index e8c2b64864..30037730b2 100644 --- a/src/components/Message/utils.tsx +++ b/src/components/Message/utils.tsx @@ -3,6 +3,7 @@ import emojiRegex from 'emoji-regex'; import type { TFunction } from 'i18next'; import type { + ChannelConfigWithInfo, LocalMessage, LocalMessageBase, MessageResponse, @@ -60,7 +61,9 @@ export const MESSAGE_ACTIONS = { pin: 'pin', quote: 'quote', react: 'react', + remindMe: 'remindMe', reply: 'reply', + saveForLater: 'saveForLater', }; export type MessageActionsArray = Array< @@ -151,6 +154,7 @@ export const getMessageActions = ( canReact, canReply, }: Capabilities, + channelConfig?: ChannelConfigWithInfo, ): MessageActionsArray => { const messageActionsAfterPermission: MessageActionsArray = []; let messageActions: MessageActionsArray = []; @@ -164,6 +168,20 @@ export const getMessageActions = ( return []; } + if ( + channelConfig?.['user_message_reminders'] && + messageActions.indexOf(MESSAGE_ACTIONS.remindMe) + ) { + messageActionsAfterPermission.push(MESSAGE_ACTIONS.remindMe); + } + + if ( + channelConfig?.['user_message_reminders'] && + messageActions.indexOf(MESSAGE_ACTIONS.saveForLater) + ) { + messageActionsAfterPermission.push(MESSAGE_ACTIONS.saveForLater); + } + if (canDelete && messageActions.indexOf(MESSAGE_ACTIONS.delete) > -1) { messageActionsAfterPermission.push(MESSAGE_ACTIONS.delete); } diff --git a/src/components/MessageActions/MessageActionsBox.tsx b/src/components/MessageActions/MessageActionsBox.tsx index a1cca670a6..2e62c0a5c6 100644 --- a/src/components/MessageActions/MessageActionsBox.tsx +++ b/src/components/MessageActions/MessageActionsBox.tsx @@ -1,18 +1,18 @@ import clsx from 'clsx'; import type { ComponentProps } from 'react'; import React from 'react'; - -import { MESSAGE_ACTIONS } from '../Message/utils'; - -import type { MessageContextValue } from '../../context'; +import { CustomMessageActionsList as DefaultCustomMessageActionsList } from './CustomMessageActionsList'; +import { RemindMeActionButton } from './RemindMeSubmenu'; +import { useMessageReminder } from '../Message'; +import { useMessageComposer } from '../MessageInput'; import { + useChatContext, useComponentContext, useMessageContext, useTranslationContext, } from '../../context'; - -import { CustomMessageActionsList as DefaultCustomMessageActionsList } from './CustomMessageActionsList'; -import { useMessageComposer } from '../MessageInput'; +import { MESSAGE_ACTIONS } from '../Message/utils'; +import type { MessageContextValue } from '../../context'; type PropsDrilledToMessageActionsBox = | 'getMessageActions' @@ -43,18 +43,19 @@ const UnMemoizedMessageActionsBox = (props: MessageActionsBoxProps) => { handleMute, handlePin, isUserMuted, - // eslint-disable-next-line @typescript-eslint/no-unused-vars mine, open, ...restDivProps } = props; + const { client } = useChatContext(); const { CustomMessageActionsList = DefaultCustomMessageActionsList } = useComponentContext('MessageActionsBox'); const { customMessageActions, message, threadList } = useMessageContext('MessageActionsBox'); const { t } = useTranslationContext('MessageActionsBox'); const messageComposer = useMessageComposer(); + const reminder = useMessageReminder(message.id); const messageActions = getMessageActions(); @@ -161,6 +162,23 @@ const UnMemoizedMessageActionsBox = (props: MessageActionsBoxProps) => { {t('Delete')} )} + {messageActions.indexOf(MESSAGE_ACTIONS.remindMe) > -1 && ( + + )} + {messageActions.indexOf(MESSAGE_ACTIONS.saveForLater) > -1 && ( + + )}
); diff --git a/src/components/MessageActions/RemindMeSubmenu.tsx b/src/components/MessageActions/RemindMeSubmenu.tsx new file mode 100644 index 0000000000..1225556362 --- /dev/null +++ b/src/components/MessageActions/RemindMeSubmenu.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { useChatContext, useMessageContext, useTranslationContext } from '../../context'; +import { ButtonWithSubmenu } from '../Dialog'; +import type { ComponentProps } from 'react'; + +export const RemindMeActionButton = ({ + className, + isMine, +}: { isMine: boolean } & ComponentProps<'button'>) => { + const { t } = useTranslationContext(); + + return ( + + {t('Remind Me')} + + ); +}; + +export const RemindMeSubmenu = () => { + const { t } = useTranslationContext(); + const { client } = useChatContext(); + const { message } = useMessageContext(); + return ( +
+ {client.reminders.scheduledOffsetsMs.map((offsetMs) => ( + + ))} + {/* todo: potential improvement to add a custom option that would trigger rendering modal with custom date picker - we need date picker */} +
+ ); +}; diff --git a/src/experimental/MessageActions/defaults.tsx b/src/experimental/MessageActions/defaults.tsx index cfbe8519e7..bd6652a5fa 100644 --- a/src/experimental/MessageActions/defaults.tsx +++ b/src/experimental/MessageActions/defaults.tsx @@ -1,23 +1,24 @@ /* eslint-disable sort-keys */ +import type { ComponentPropsWithoutRef } from 'react'; import React from 'react'; -import { isUserMuted } from '../../components'; +import { isUserMuted, useMessageComposer, useMessageReminder } from '../../components'; import { ReactionIcon as DefaultReactionIcon, ThreadIcon, } from '../../components/Message/icons'; import { ReactionSelectorWithButton } from '../../components/Reactions/ReactionSelectorWithButton'; import { useChatContext, useMessageContext, useTranslationContext } from '../../context'; -import { useMessageComposer } from '../../components'; - -import type { ComponentPropsWithoutRef } from 'react'; - +import { RemindMeActionButton } from '../../components/MessageActions/RemindMeSubmenu'; import type { MessageActionSetItem } from './MessageActions'; +const msgActionsBoxButtonClassName = + 'str-chat__message-actions-list-item-button' as const; + export const DefaultDropdownActionButton = ({ 'aria-selected': ariaSelected = 'false', children, - className = 'str-chat__message-actions-list-item-button', + className = msgActionsBoxButtonClassName, role = 'option', ...rest }: ComponentPropsWithoutRef<'button'>) => ( @@ -113,6 +114,33 @@ const DefaultMessageActionComponents = { ); }, + RemindMe() { + const { isMyMessage } = useMessageContext(); + return ( + + ); + }, + SaveForLater() { + const { client } = useChatContext(); + const { message } = useMessageContext(); + const { t } = useTranslationContext(); + const reminder = useMessageReminder(message.id); + + return ( + + reminder + ? client.reminders.deleteReminder(reminder.id) + : client.reminders.createReminder({ messageId: message.id }) + } + > + {reminder ? t('Remove reminder') : t('Save for later')} + + ); + }, }, quick: { React() { @@ -183,4 +211,14 @@ export const defaultMessageActionSet: MessageActionSetItem[] = [ placement: 'dropdown', type: 'markUnread', }, + { + Component: DefaultMessageActionComponents.dropdown.RemindMe, + placement: 'dropdown', + type: 'remindMe', + }, + { + Component: DefaultMessageActionComponents.dropdown.SaveForLater, + placement: 'dropdown', + type: 'saveForLater', + }, ] as const; diff --git a/src/i18n/Streami18n.ts b/src/i18n/Streami18n.ts index 0a4f53bb2b..3b596d163b 100644 --- a/src/i18n/Streami18n.ts +++ b/src/i18n/Streami18n.ts @@ -5,6 +5,7 @@ import updateLocale from 'dayjs/plugin/updateLocale'; import LocalizedFormat from 'dayjs/plugin/localizedFormat'; import localeData from 'dayjs/plugin/localeData'; import relativeTime from 'dayjs/plugin/relativeTime'; +import duration from 'dayjs/plugin/duration'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; import { defaultTranslatorFunction, predefinedFormatters } from './utils'; @@ -526,6 +527,7 @@ export class Streami18n { this.DateTimeParser.extend(calendar); this.DateTimeParser.extend(localeData); this.DateTimeParser.extend(relativeTime); + this.DateTimeParser.extend(duration); } } catch (error) { throw Error( diff --git a/src/i18n/de.json b/src/i18n/de.json index b57e471526..5ea9016c3d 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -27,6 +27,7 @@ "Download attachment {{ name }}": "Anhang {{ name }} herunterladen", "Drag your files here": "Ziehen Sie Ihre Dateien hierher", "Drag your files here to add to your post": "Ziehen Sie Ihre Dateien hierher, um sie Ihrem Beitrag hinzuzufügen", + "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", "Edit Message": "Nachricht bearbeiten", "Edit message request failed": "Anfrage zum Bearbeiten der Nachricht fehlgeschlagen", "Edited": "Bearbeitet", @@ -93,8 +94,12 @@ "Question": "Frage", "Quote": "Zitieren", "Recording format is not supported and cannot be reproduced": "Aufnahmeformat wird nicht unterstützt und kann nicht wiedergegeben werden", + "Remind Me": "Remind Me", + "Remove reminder": "Remove reminder", "Reply": "Antworten", "Reply to Message": "Auf Nachricht antworten", + "Save for later": "Save for later", + "Saved for later": "Saved for later", "Search": "Suche", "Searching...": "Suchen...", "See all options ({{count}})_one": "Alle Optionen anzeigen ({{count}})", @@ -158,6 +163,7 @@ "aria/Open Reaction Selector": "Reaktionsauswahl öffnen", "aria/Open Thread": "Thread öffnen", "aria/Reaction list": "Reaktionsliste", + "aria/Remind Me Options": "aria/Remind Me Options", "aria/Remove attachment": "Anhang entfernen", "aria/Retry upload": "Upload erneut versuchen", "aria/Search results": "Suchergebnisse", @@ -166,6 +172,7 @@ "aria/Stop AI Generation": "KI-Generierung stoppen", "ban-command-args": "[@Benutzername] [Text]", "ban-command-description": "Einen Benutzer verbannen", + "duration/Remind Me": "duration/Remind Me", "giphy-command-args": "[Text]", "giphy-command-description": "Poste ein zufälliges Gif in den Kanal", "live": "live", diff --git a/src/i18n/en.json b/src/i18n/en.json index dd0ec0df8a..2a5d241f47 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -27,6 +27,7 @@ "Download attachment {{ name }}": "Download attachment {{ name }}", "Drag your files here": "Drag your files here", "Drag your files here to add to your post": "Drag your files here to add to your post", + "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", "Edit Message": "Edit Message", "Edit message request failed": "Edit message request failed", "Edited": "Edited", @@ -93,8 +94,12 @@ "Question": "Question", "Quote": "Quote", "Recording format is not supported and cannot be reproduced": "Recording format is not supported and cannot be reproduced", + "Remind Me": "Remind Me", + "Remove reminder": "Remove reminder", "Reply": "Reply", "Reply to Message": "Reply to Message", + "Save for later": "Save for later", + "Saved for later": "Saved for later", "Search": "Search", "Searching...": "Searching...", "See all options ({{count}})_one": "See all options ({{count}})", @@ -158,12 +163,15 @@ "aria/Open Reaction Selector": "Open Reaction Selector", "aria/Open Thread": "Open Thread", "aria/Reaction list": "Reaction list", + "aria/Remind Me Options": "aria/Remind Me Options", "aria/Remove attachment": "Remove attachment", "aria/Retry upload": "Retry upload", "aria/Search results": "Search results", "aria/Search results header filter button": "Search results header filter button", "aria/Send": "Send", "aria/Stop AI Generation": "Stop AI Generation", + "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", + "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", "live": "live", "replyCount_one": "1 reply", "replyCount_other": "{{ count }} replies", diff --git a/src/i18n/es.json b/src/i18n/es.json index c59165beed..81fd5afb61 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -27,6 +27,7 @@ "Download attachment {{ name }}": "Descargar adjunto {{ name }}", "Drag your files here": "Arrastra tus archivos aquí", "Drag your files here to add to your post": "Arrastra tus archivos aquí para agregarlos a tu publicación", + "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", "Edit Message": "Editar mensaje", "Edit message request failed": "Error al editar la solicitud de mensaje", "Edited": "Editado", @@ -93,8 +94,12 @@ "Question": "Pregunta", "Quote": "Citar", "Recording format is not supported and cannot be reproduced": "El formato de grabación no es compatible y no se puede reproducir", + "Remind Me": "Remind Me", + "Remove reminder": "Remove reminder", "Reply": "Responder", "Reply to Message": "Responder al mensaje", + "Save for later": "Save for later", + "Saved for later": "Saved for later", "Search": "Buscar", "Searching...": "Buscando...", "See all options ({{count}})_many": "Ver todas las opciones ({{count}})", @@ -161,6 +166,7 @@ "aria/Open Reaction Selector": "Abrir selector de reacciones", "aria/Open Thread": "Abrir hilo", "aria/Reaction list": "Lista de reacciones", + "aria/Remind Me Options": "aria/Remind Me Options", "aria/Remove attachment": "Eliminar adjunto", "aria/Retry upload": "Reintentar carga", "aria/Search results": "Resultados de búsqueda", @@ -169,6 +175,7 @@ "aria/Stop AI Generation": "Detener generación de IA", "ban-command-args": "[@usuario] [texto]", "ban-command-description": "Prohibir a un usuario", + "duration/Remind Me": "duration/Remind Me", "giphy-command-args": "[texto]", "giphy-command-description": "Publicar un gif aleatorio en el canal", "live": "En vivo", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 8634d30ccc..03d21b91e6 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -27,6 +27,7 @@ "Download attachment {{ name }}": "Télécharger la pièce jointe {{ name }}", "Drag your files here": "Glissez vos fichiers ici", "Drag your files here to add to your post": "Glissez vos fichiers ici pour les ajouter à votre publication", + "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", "Edit Message": "Éditer un message", "Edit message request failed": "Échec de la demande de modification du message", "Edited": "Modifié", @@ -93,8 +94,12 @@ "Question": "Question", "Quote": "Citer", "Recording format is not supported and cannot be reproduced": "Le format d'enregistrement n'est pas pris en charge et ne peut pas être reproduit", + "Remind Me": "Remind Me", + "Remove reminder": "Remove reminder", "Reply": "Répondre", "Reply to Message": "Répondre au message", + "Save for later": "Save for later", + "Saved for later": "Saved for later", "Search": "Rechercher", "Searching...": "Recherche en cours...", "See all options ({{count}})_many": "Voir toutes les options ({{count}})", @@ -161,6 +166,7 @@ "aria/Open Reaction Selector": "Ouvrir le sélecteur de réactions", "aria/Open Thread": "Ouvrir le fil", "aria/Reaction list": "Liste des réactions", + "aria/Remind Me Options": "aria/Remind Me Options", "aria/Remove attachment": "Supprimer la pièce jointe", "aria/Retry upload": "Réessayer le téléchargement", "aria/Search results": "Résultats de recherche", @@ -169,6 +175,7 @@ "aria/Stop AI Generation": "Arrêter la génération d'IA", "ban-command-args": "[@nomdutilisateur] [texte]", "ban-command-description": "Bannir un utilisateur", + "duration/Remind Me": "duration/Remind Me", "giphy-command-args": "[texte]", "giphy-command-description": "Poster un GIF aléatoire dans le canal", "live": "en direct", diff --git a/src/i18n/hi.json b/src/i18n/hi.json index 0271c652e1..2e551853c0 100644 --- a/src/i18n/hi.json +++ b/src/i18n/hi.json @@ -27,6 +27,7 @@ "Download attachment {{ name }}": "अनुलग्नक {{ name }} डाउनलोड करें", "Drag your files here": "अपनी फ़ाइलें यहाँ खींचें", "Drag your files here to add to your post": "अपनी फ़ाइलें यहाँ खींचें और अपने पोस्ट में जोड़ने के लिए", + "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", "Edit Message": "मैसेज में बदलाव करे", "Edit message request failed": "संदेश संपादित करने का अनुरोध विफल रहा", "Edited": "संपादित", @@ -94,8 +95,12 @@ "Question": "प्रश्न", "Quote": "उद्धरण", "Recording format is not supported and cannot be reproduced": "रेकॉर्डिंग फ़ॉर्मेट समर्थित नहीं है और पुनः उत्पन्न नहीं किया जा सकता", + "Remind Me": "Remind Me", + "Remove reminder": "Remove reminder", "Reply": "जवाब दे दो", "Reply to Message": "संदेश का जवाब दें", + "Save for later": "Save for later", + "Saved for later": "Saved for later", "Search": "खोज", "Searching...": "खोज कर...", "See all options ({{count}})_one": "सभी विकल्प देखें ({{count}})", @@ -159,6 +164,7 @@ "aria/Open Reaction Selector": "प्रतिक्रिया चयनकर्ता खोलें", "aria/Open Thread": "थ्रेड खोलें", "aria/Reaction list": "प्रतिक्रिया सूची", + "aria/Remind Me Options": "aria/Remind Me Options", "aria/Remove attachment": "संलग्नक हटाएं", "aria/Retry upload": "अपलोड पुनः प्रयास करें", "aria/Search results": "खोज परिणाम", @@ -167,6 +173,7 @@ "aria/Stop AI Generation": "एआई जनरेशन रोकें", "ban-command-args": "[@उपयोगकर्तनाम] [पाठ]", "ban-command-description": "एक उपयोगकर्ता को प्रतिषेधित करें", + "duration/Remind Me": "duration/Remind Me", "giphy-command-args": "[पाठ]", "giphy-command-description": "चैनल पर एक क्रॉफिल जीआइएफ पोस्ट करें", "live": "लाइव", diff --git a/src/i18n/it.json b/src/i18n/it.json index 8aa9468008..96c2540862 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -27,6 +27,7 @@ "Download attachment {{ name }}": "Scarica l'allegato {{ name }}", "Drag your files here": "Trascina i tuoi file qui", "Drag your files here to add to your post": "Trascina i tuoi file qui per aggiungerli al tuo post", + "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", "Edit Message": "Modifica messaggio", "Edit message request failed": "Richiesta di modifica del messaggio non riuscita", "Edited": "Modificato", @@ -93,8 +94,12 @@ "Question": "Domanda", "Quote": "Citazione", "Recording format is not supported and cannot be reproduced": "Il formato di registrazione non è supportato e non può essere riprodotto", + "Remind Me": "Remind Me", + "Remove reminder": "Remove reminder", "Reply": "Rispondi", "Reply to Message": "Rispondi al messaggio", + "Save for later": "Save for later", + "Saved for later": "Saved for later", "Search": "Cerca", "Searching...": "Ricerca in corso...", "See all options ({{count}})_many": "Vedi tutte le opzioni ({{count}})", @@ -161,6 +166,7 @@ "aria/Open Reaction Selector": "Apri il selettore di reazione", "aria/Open Thread": "Apri discussione", "aria/Reaction list": "Elenco delle reazioni", + "aria/Remind Me Options": "aria/Remind Me Options", "aria/Remove attachment": "Rimuovi allegato", "aria/Retry upload": "Riprova caricamento", "aria/Search results": "Risultati della ricerca", @@ -169,6 +175,7 @@ "aria/Stop AI Generation": "Interrompi generazione IA", "ban-command-args": "[@nomeutente] [testo]", "ban-command-description": "Vietare un utente", + "duration/Remind Me": "duration/Remind Me", "giphy-command-args": "[testo]", "giphy-command-description": "Pubblica un gif casuale sul canale", "live": "live", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index eced5707be..9a44d0205f 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -27,6 +27,7 @@ "Download attachment {{ name }}": "添付ファイル {{ name }} をダウンロード", "Drag your files here": "ここにファイルをドラッグ", "Drag your files here to add to your post": "投稿に追加するためにここにファイルをドラッグ", + "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", "Edit Message": "メッセージを編集", "Edit message request failed": "メッセージの編集要求が失敗しました", "Edited": "編集済み", @@ -93,8 +94,12 @@ "Question": "質問", "Quote": "引用", "Recording format is not supported and cannot be reproduced": "録音形式はサポートされておらず、再生できません", + "Remind Me": "Remind Me", + "Remove reminder": "Remove reminder", "Reply": "返事", "Reply to Message": "メッセージに返信", + "Save for later": "Save for later", + "Saved for later": "Saved for later", "Search": "探す", "Searching...": "検索中...", "See all options ({{count}})_other": "すべてのオプションを見る ({{count}})", @@ -155,6 +160,7 @@ "aria/Open Reaction Selector": "リアクションセレクターを開く", "aria/Open Thread": "スレッドを開く", "aria/Reaction list": "リアクション一覧", + "aria/Remind Me Options": "aria/Remind Me Options", "aria/Remove attachment": "添付ファイルを削除", "aria/Retry upload": "アップロードを再試行", "aria/Search results": "検索結果", @@ -163,6 +169,7 @@ "aria/Stop AI Generation": "AI生成を停止", "ban-command-args": "[@ユーザ名] [テキスト]", "ban-command-description": "ユーザーを禁止する", + "duration/Remind Me": "duration/Remind Me", "giphy-command-args": "[テキスト]", "giphy-command-description": "チャンネルにランダムなGIFを投稿する", "live": "ライブ", diff --git a/src/i18n/ko.json b/src/i18n/ko.json index a42a75d3d7..f220fbc587 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -27,6 +27,7 @@ "Download attachment {{ name }}": "첨부 파일 {{ name }} 다운로드", "Drag your files here": "여기로 파일을 끌어다 놓으세요", "Drag your files here to add to your post": "게시물에 추가하려면 파일을 여기로 끌어다 놓으세요", + "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", "Edit Message": "메시지 수정", "Edit message request failed": "메시지 수정 요청 실패", "Edited": "편집됨", @@ -93,8 +94,12 @@ "Question": "질문", "Quote": "인용", "Recording format is not supported and cannot be reproduced": "녹음 형식이 지원되지 않으므로 재생할 수 없습니다", + "Remind Me": "Remind Me", + "Remove reminder": "Remove reminder", "Reply": "답장", "Reply to Message": "메시지에 답장", + "Save for later": "Save for later", + "Saved for later": "Saved for later", "Search": "찾다", "Searching...": "수색...", "See all options ({{count}})_other": "모든 옵션 보기 ({{count}})", @@ -155,6 +160,7 @@ "aria/Open Reaction Selector": "반응 선택기 열기", "aria/Open Thread": "스레드 열기", "aria/Reaction list": "반응 목록", + "aria/Remind Me Options": "aria/Remind Me Options", "aria/Remove attachment": "첨부 파일 제거", "aria/Retry upload": "업로드 다시 시도", "aria/Search results": "검색 결과", @@ -163,6 +169,7 @@ "aria/Stop AI Generation": "AI 생성 중지", "ban-command-args": "[@사용자이름] [텍스트]", "ban-command-description": "사용자를 차단", + "duration/Remind Me": "duration/Remind Me", "giphy-command-args": "[텍스트]", "giphy-command-description": "채널에 무작위 GIF 게시", "live": "라이브", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index c68452dc78..40ece59734 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -27,6 +27,7 @@ "Download attachment {{ name }}": "Bijlage {{ name }} downloaden", "Drag your files here": "Sleep je bestanden hier naartoe", "Drag your files here to add to your post": "Sleep je bestanden hier naartoe om aan je bericht toe te voegen", + "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", "Edit Message": "Bericht bewerken", "Edit message request failed": "Verzoek om bericht bewerken mislukt", "Edited": "Bewerkt", @@ -93,8 +94,12 @@ "Question": "Vraag", "Quote": "Citeer", "Recording format is not supported and cannot be reproduced": "Opnameformaat wordt niet ondersteund en kan niet worden gereproduceerd", + "Remind Me": "Remind Me", + "Remove reminder": "Remove reminder", "Reply": "Antwoord", "Reply to Message": "Antwoord op bericht", + "Save for later": "Save for later", + "Saved for later": "Saved for later", "Search": "Zoeken", "Searching...": "Zoeken...", "See all options ({{count}})_one": "Bekijk alle opties ({{count}})", @@ -158,6 +163,7 @@ "aria/Open Reaction Selector": "Reactiekiezer openen", "aria/Open Thread": "Draad openen", "aria/Reaction list": "Reactielijst", + "aria/Remind Me Options": "aria/Remind Me Options", "aria/Remove attachment": "Bijlage verwijderen", "aria/Retry upload": "Upload opnieuw proberen", "aria/Search results": "Zoekresultaten", @@ -166,6 +172,7 @@ "aria/Stop AI Generation": "AI-generatie stoppen", "ban-command-args": "[@gebruikersnaam] [tekst]", "ban-command-description": "Een gebruiker verbannen", + "duration/Remind Me": "duration/Remind Me", "giphy-command-args": "[tekst]", "giphy-command-description": "Plaats een willekeurige gif in het kanaal", "live": "live", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index d7a01f547d..029bde527c 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -27,6 +27,7 @@ "Download attachment {{ name }}": "Baixar anexo {{ name }}", "Drag your files here": "Arraste seus arquivos aqui", "Drag your files here to add to your post": "Arraste seus arquivos aqui para adicionar ao seu post", + "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", "Edit Message": "Editar Mensagem", "Edit message request failed": "O pedido de edição da mensagem falhou", "Edited": "Editada", @@ -93,8 +94,12 @@ "Question": "Pergunta", "Quote": "Citar", "Recording format is not supported and cannot be reproduced": "Formato de gravação não é suportado e não pode ser reproduzido", + "Remind Me": "Remind Me", + "Remove reminder": "Remove reminder", "Reply": "Responder", "Reply to Message": "Responder à mensagem", + "Save for later": "Save for later", + "Saved for later": "Saved for later", "Search": "Buscar", "Searching...": "Buscando...", "See all options ({{count}})_many": "Ver todas as opções ({{count}})", @@ -161,6 +166,7 @@ "aria/Open Reaction Selector": "Abrir seletor de reações", "aria/Open Thread": "Abrir tópico", "aria/Reaction list": "Lista de reações", + "aria/Remind Me Options": "aria/Remind Me Options", "aria/Remove attachment": "Remover anexo", "aria/Retry upload": "Tentar upload novamente", "aria/Search results": "Resultados da pesquisa", @@ -169,6 +175,7 @@ "aria/Stop AI Generation": "Parar geração de IA", "ban-command-args": "[@nomedeusuário] [texto]", "ban-command-description": "Banir um usuário", + "duration/Remind Me": "duration/Remind Me", "giphy-command-args": "[texto]", "giphy-command-description": "Postar um gif aleatório no canal", "live": "ao vivo", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index ad5856beb7..35619b8f06 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -27,6 +27,7 @@ "Download attachment {{ name }}": "Скачать вложение {{ name }}", "Drag your files here": "Перетащите ваши файлы сюда", "Drag your files here to add to your post": "Перетащите ваши файлы сюда, чтобы добавить их в ваш пост", + "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", "Edit Message": "Редактировать сообщение", "Edit message request failed": "Не удалось изменить запрос сообщения", "Edited": "Отредактировано", @@ -93,8 +94,12 @@ "Question": "Вопрос", "Quote": "Цитировать", "Recording format is not supported and cannot be reproduced": "Формат записи не поддерживается и не может быть воспроизведен", + "Remind Me": "Remind Me", + "Remove reminder": "Remove reminder", "Reply": "Ответить", "Reply to Message": "Ответить на сообщение", + "Save for later": "Save for later", + "Saved for later": "Saved for later", "Search": "Поиск", "Searching...": "Ищем...", "See all options ({{count}})_few": "Посмотреть все варианты ({{count}})", @@ -164,6 +169,7 @@ "aria/Open Reaction Selector": "Открыть селектор реакций", "aria/Open Thread": "Открыть тему", "aria/Reaction list": "Список реакций", + "aria/Remind Me Options": "aria/Remind Me Options", "aria/Remove attachment": "Удалить вложение", "aria/Retry upload": "Повторить загрузку", "aria/Search results": "Результаты поиска", @@ -172,6 +178,7 @@ "aria/Stop AI Generation": "Остановить генерацию ИИ", "ban-command-args": "[@имяпользователя] [текст]", "ban-command-description": "Заблокировать пользователя", + "duration/Remind Me": "duration/Remind Me", "giphy-command-args": "[текст]", "giphy-command-description": "Опубликовать случайную GIF-анимацию в канале", "live": "В прямом эфире", diff --git a/src/i18n/tr.json b/src/i18n/tr.json index a703ddafc2..652ff5fc65 100644 --- a/src/i18n/tr.json +++ b/src/i18n/tr.json @@ -27,6 +27,7 @@ "Download attachment {{ name }}": "Ek {{ name }}'i indir", "Drag your files here": "Dosyalarınızı buraya sürükleyin", "Drag your files here to add to your post": "Gönderinize eklemek için dosyalarınızı buraya sürükleyin", + "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", "Edit Message": "Mesajı Düzenle", "Edit message request failed": "Mesaj düzenleme isteği başarısız oldu", "Edited": "Düzenlendi", @@ -93,8 +94,12 @@ "Question": "Soru", "Quote": "Alıntı", "Recording format is not supported and cannot be reproduced": "Kayıt formatı desteklenmiyor ve çoğaltılamıyor", + "Remind Me": "Remind Me", + "Remove reminder": "Remove reminder", "Reply": "Cevapla", "Reply to Message": "Mesaja Cevapla", + "Save for later": "Save for later", + "Saved for later": "Saved for later", "Search": "Arama", "Searching...": "Aranıyor...", "See all options ({{count}})_one": "Tüm seçenekleri göster ({{count}})", @@ -158,6 +163,7 @@ "aria/Open Reaction Selector": "Tepki Seçiciyi Aç", "aria/Open Thread": "Konuyu Aç", "aria/Reaction list": "Tepki listesi", + "aria/Remind Me Options": "aria/Remind Me Options", "aria/Remove attachment": "Eki kaldır", "aria/Retry upload": "Yüklemeyi Tekrar Dene", "aria/Search results": "Arama sonuçları", @@ -166,6 +172,7 @@ "aria/Stop AI Generation": "Yapay Zeka Üretimini Durdur", "ban-command-args": "[@kullanıcıadı] [metin]", "ban-command-description": "Bir kullanıcıyı yasakla", + "duration/Remind Me": "duration/Remind Me", "giphy-command-args": "[metin]", "giphy-command-description": "Rastgele bir gif'i kanala gönder", "live": "canlı", diff --git a/src/i18n/types.ts b/src/i18n/types.ts index a923a92e8a..f4c214647c 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -17,6 +17,60 @@ export type TimestampFormatterOptions = { format?: string; }; +/** + * import dayjs from 'dayjs'; + * import duration from 'dayjs/plugin/duration'; + * + * dayjs.extend(duration); + * + * // Basic formatting + * dayjs.duration(1000).format('HH:mm:ss'); // "00:00:01" + * dayjs.duration(3661000).format('HH:mm:ss'); // "01:01:01" + * + * // Different format tokens + * dayjs.duration(3661000).format('D[d] H[h] m[m] s[s]'); // "0d 1h 1m 1s" + * dayjs.duration(3661000).format('D [days] H [hours] m [minutes] s [seconds]'); // "0 days 1 hours 1 minutes 1 seconds" + * + * // Zero padding + * dayjs.duration(1000).format('HH:mm:ss'); // "00:00:01" + * dayjs.duration(1000).format('H:m:s'); // "0:0:1" + * + * // Different units + * dayjs.duration(3661000).format('D'); // "0" + * dayjs.duration(3661000).format('H'); // "1" + * dayjs.duration(3661000).format('m'); // "1" + * dayjs.duration(3661000).format('s'); // "1" + * + * // Complex examples + * dayjs.duration(3661000).format('DD:HH:mm:ss'); // "00:01:01:01" + * dayjs.duration(3661000).format('D [days] HH:mm:ss'); // "0 days 01:01:01" + * dayjs.duration(3661000).format('H[h] m[m] s[s]'); // "1h 1m 1s" + * + * // Negative durations + * dayjs.duration(-3661000).format('HH:mm:ss'); // "-01:01:01" + * + * // Long durations + * dayjs.duration(86400000).format('D [days]'); // "1 days" + * dayjs.duration(2592000000).format('M [months]'); // "30 months" + * + * + * Format tokens: + * D - days + * H - hours + * m - minutes + * s - seconds + * S - milliseconds + * M - months + * Y - years + * You can also use: + * HH, mm, ss for zero-padded numbers + * [text] for literal text + */ +export type DurationFormatterOptions = { + format?: string; + withSuffix?: boolean; +}; + export type TDateTimeParserInput = string | number | Date; export type TDateTimeParserOutput = string | number | Date | Dayjs.Dayjs | Moment; export type TDateTimeParser = (input?: TDateTimeParserInput) => TDateTimeParserOutput; @@ -49,5 +103,6 @@ export type DateFormatterOptions = TimestampFormatterOptions & { export type CustomFormatters = Record>; export type PredefinedFormatters = { + durationFormatter: FormatterFactory; timestampFormatter: FormatterFactory; }; diff --git a/src/i18n/utils.ts b/src/i18n/utils.ts index 431ccc17ed..b23279b557 100644 --- a/src/i18n/utils.ts +++ b/src/i18n/utils.ts @@ -1,9 +1,11 @@ -import Dayjs from 'dayjs'; +import Dayjs, { isDayjs } from 'dayjs'; +import type { Duration as DayjsDuration } from 'dayjs/plugin/duration'; import type { TFunction } from 'i18next'; import type { Moment } from 'moment-timezone'; import type { DateFormatterOptions, + DurationFormatterOptions, PredefinedFormatters, SupportedTranslations, TDateTimeParserInput, @@ -95,6 +97,16 @@ export function getDateString({ } export const predefinedFormatters: PredefinedFormatters = { + durationFormatter: + (streamI18n) => + (value, _, { format, withSuffix }: DurationFormatterOptions) => { + if (format && isDayjs(streamI18n.DateTimeParser)) { + return (streamI18n.DateTimeParser.duration(value) as DayjsDuration).format( + format, + ); + } + return streamI18n.DateTimeParser.duration(value).humanize(!!withSuffix); + }, timestampFormatter: (streamI18n) => ( From 74c73d522d29f4b585911235f4076388ba02d721 Mon Sep 17 00:00:00 2001 From: martincupela Date: Tue, 3 Jun 2025 12:53:29 +0200 Subject: [PATCH 02/11] test: add message actions reminder test and fix broken tests --- .../Message/__tests__/utils.test.js | 13 +++++++-- src/components/Message/utils.tsx | 28 +++++++++---------- .../__tests__/MessageActionsBox.test.js | 14 ++++++---- .../Poll/__tests__/PollCreationDialog.test.js | 2 +- 4 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/components/Message/__tests__/utils.test.js b/src/components/Message/__tests__/utils.test.js index cf6b7d9ee9..0def97d792 100644 --- a/src/components/Message/__tests__/utils.test.js +++ b/src/components/Message/__tests__/utils.test.js @@ -102,9 +102,18 @@ describe('Message utils', () => { }, ); - it('should return all message actions if actions are set to true', () => { + it('should return all message actions not depending on channel config if actions are set to true', () => { const result = getMessageActions(true, defaultCapabilities); - expect(result).toStrictEqual(actions); + expect(result).toStrictEqual( + actions.filter((a) => !['remindMe', 'saveForLater'].includes(a)), + ); + }); + + it('should include reminder actions if enabled in channel config', () => { + const result = getMessageActions(true, defaultCapabilities, { + user_message_reminders: true, + }); + expect(result).toEqual(actions); }); it.each([ diff --git a/src/components/Message/utils.tsx b/src/components/Message/utils.tsx index 30037730b2..57464fbf2c 100644 --- a/src/components/Message/utils.tsx +++ b/src/components/Message/utils.tsx @@ -168,20 +168,6 @@ export const getMessageActions = ( return []; } - if ( - channelConfig?.['user_message_reminders'] && - messageActions.indexOf(MESSAGE_ACTIONS.remindMe) - ) { - messageActionsAfterPermission.push(MESSAGE_ACTIONS.remindMe); - } - - if ( - channelConfig?.['user_message_reminders'] && - messageActions.indexOf(MESSAGE_ACTIONS.saveForLater) - ) { - messageActionsAfterPermission.push(MESSAGE_ACTIONS.saveForLater); - } - if (canDelete && messageActions.indexOf(MESSAGE_ACTIONS.delete) > -1) { messageActionsAfterPermission.push(MESSAGE_ACTIONS.delete); } @@ -214,10 +200,24 @@ export const getMessageActions = ( messageActionsAfterPermission.push(MESSAGE_ACTIONS.react); } + if ( + channelConfig?.['user_message_reminders'] && + messageActions.indexOf(MESSAGE_ACTIONS.remindMe) + ) { + messageActionsAfterPermission.push(MESSAGE_ACTIONS.remindMe); + } + if (canReply && messageActions.indexOf(MESSAGE_ACTIONS.reply) > -1) { messageActionsAfterPermission.push(MESSAGE_ACTIONS.reply); } + if ( + channelConfig?.['user_message_reminders'] && + messageActions.indexOf(MESSAGE_ACTIONS.saveForLater) + ) { + messageActionsAfterPermission.push(MESSAGE_ACTIONS.saveForLater); + } + return messageActionsAfterPermission; }; diff --git a/src/components/MessageActions/__tests__/MessageActionsBox.test.js b/src/components/MessageActions/__tests__/MessageActionsBox.test.js index 1f25808fcc..14a62bad58 100644 --- a/src/components/MessageActions/__tests__/MessageActionsBox.test.js +++ b/src/components/MessageActions/__tests__/MessageActionsBox.test.js @@ -89,7 +89,7 @@ describe('MessageActionsBox', () => { it('should not show any of the action buttons if no actions are returned by getMessageActions', async () => { const { result: { container, queryByText }, - } = await renderComponent({}); + } = await renderComponent({ message: generateMessage() }); expect(queryByText('Flag')).not.toBeInTheDocument(); expect(queryByText('Mute')).not.toBeInTheDocument(); expect(queryByText('Unmute')).not.toBeInTheDocument(); @@ -106,7 +106,7 @@ describe('MessageActionsBox', () => { const handleFlag = jest.fn(); const { result: { container, getByText }, - } = await renderComponent({ handleFlag }); + } = await renderComponent({ handleFlag, message: generateMessage() }); await act(async () => { await fireEvent.click(getByText('Flag')); }); @@ -123,6 +123,7 @@ describe('MessageActionsBox', () => { } = await renderComponent({ handleMute, isUserMuted: () => false, + message: generateMessage(), }); await act(async () => { await fireEvent.click(getByText('Mute')); @@ -140,6 +141,7 @@ describe('MessageActionsBox', () => { } = await renderComponent({ handleMute, isUserMuted: () => true, + message: generateMessage(), }); await act(async () => { await fireEvent.click(getByText('Unmute')); @@ -154,7 +156,7 @@ describe('MessageActionsBox', () => { const handleEdit = jest.fn(); const { result: { container, getByText }, - } = await renderComponent({ handleEdit }); + } = await renderComponent({ handleEdit, message: generateMessage() }); await act(async () => { await fireEvent.click(getByText('Edit Message')); }); @@ -168,7 +170,7 @@ describe('MessageActionsBox', () => { const handleDelete = jest.fn(); const { result: { container, getByText }, - } = await renderComponent({ handleDelete }); + } = await renderComponent({ handleDelete, message: generateMessage() }); await act(async () => { await fireEvent.click(getByText('Delete')); }); @@ -180,7 +182,7 @@ describe('MessageActionsBox', () => { it('should call the handlePin prop if the pin button is clicked', async () => { getMessageActionsMock.mockImplementationOnce(() => ['pin']); const handlePin = jest.fn(); - const message = generateMessage({ pinned: false }); + const message = generateMessage({ message: generateMessage(), pinned: false }); const { result: { container, getByText }, } = await renderComponent({ handlePin, message }); @@ -195,7 +197,7 @@ describe('MessageActionsBox', () => { it('should call the handlePin prop if the unpin button is clicked', async () => { getMessageActionsMock.mockImplementationOnce(() => ['pin']); const handlePin = jest.fn(); - const message = generateMessage({ pinned: true }); + const message = generateMessage({ message: generateMessage(), pinned: true }); const { result: { container, getByText }, } = await renderComponent({ handlePin, message }); diff --git a/src/components/Poll/__tests__/PollCreationDialog.test.js b/src/components/Poll/__tests__/PollCreationDialog.test.js index b7ba9f62f1..3330ef03e8 100644 --- a/src/components/Poll/__tests__/PollCreationDialog.test.js +++ b/src/components/Poll/__tests__/PollCreationDialog.test.js @@ -100,7 +100,7 @@ describe('PollCreationDialog', () => { await fireEvent.blur(nameInput); }); expect(screen.getByTestId(NAME_INPUT_FIELD_ERROR_TEST_ID)).toHaveTextContent( - 'Name is required', + 'Question is required', ); expect(nameInput).toHaveValue(''); expect(screen.getByText(CANCEL_BUTTON_TEXT)).toBeEnabled(); From a3642798cc91d676c691d28b67531a048aa17c6e Mon Sep 17 00:00:00 2001 From: martincupela Date: Tue, 3 Jun 2025 14:21:42 +0200 Subject: [PATCH 03/11] feat: allow for custom ReminderNotification component --- src/components/Channel/Channel.tsx | 3 ++ src/components/Message/MessageSimple.tsx | 3 +- .../Message/__tests__/MessageSimple.test.js | 23 ++++++++++++ src/components/Message/index.ts | 1 + src/context/ComponentContext.tsx | 2 ++ src/mock-builders/generator/message.js | 36 +++++++++++-------- src/mock-builders/generator/reminder.ts | 33 +++++++++++++++++ 7 files changed, 85 insertions(+), 16 deletions(-) create mode 100644 src/mock-builders/generator/reminder.ts diff --git a/src/components/Channel/Channel.tsx b/src/components/Channel/Channel.tsx index 639ddaa0af..43f7539d82 100644 --- a/src/components/Channel/Channel.tsx +++ b/src/components/Channel/Channel.tsx @@ -140,6 +140,7 @@ type ChannelPropsForwardedToComponentContext = Pick< | 'ReactionSelector' | 'ReactionsList' | 'ReactionsListModal' + | 'ReminderNotification' | 'SendButton' | 'StartRecordingAudioButton' | 'TextareaComposer' @@ -1226,6 +1227,7 @@ const ChannelInner = ( ReactionSelector: props.ReactionSelector, ReactionsList: props.ReactionsList, ReactionsListModal: props.ReactionsListModal, + ReminderNotification: props.ReminderNotification, SendButton: props.SendButton, StartRecordingAudioButton: props.StartRecordingAudioButton, StopAIGenerationButton: props.StopAIGenerationButton, @@ -1289,6 +1291,7 @@ const ChannelInner = ( props.ReactionSelector, props.ReactionsList, props.ReactionsListModal, + props.ReminderNotification, props.SendButton, props.StartRecordingAudioButton, props.StopAIGenerationButton, diff --git a/src/components/Message/MessageSimple.tsx b/src/components/Message/MessageSimple.tsx index 63a1f71a5e..979ae9b572 100644 --- a/src/components/Message/MessageSimple.tsx +++ b/src/components/Message/MessageSimple.tsx @@ -37,7 +37,7 @@ import type { MessageUIComponentProps } from './types'; import { StreamedMessageText as DefaultStreamedMessageText } from './StreamedMessageText'; import { isDateSeparatorMessage } from '../MessageList'; -import { ReminderNotification } from './ReminderNotification'; +import { ReminderNotification as DefaultReminderNotification } from './ReminderNotification'; import { useMessageReminder } from './hooks'; type MessageSimpleWithContextProps = MessageContextValue; @@ -81,6 +81,7 @@ const MessageSimpleWithContext = (props: MessageSimpleWithContextProps) => { MessageStatus = DefaultMessageStatus, MessageTimestamp = DefaultMessageTimestamp, ReactionsList = DefaultReactionList, + ReminderNotification = DefaultReminderNotification, StreamedMessageText = DefaultStreamedMessageText, PinIndicator, } = useComponentContext('MessageSimple'); diff --git a/src/components/Message/__tests__/MessageSimple.test.js b/src/components/Message/__tests__/MessageSimple.test.js index c158ad202e..1c1d2e2466 100644 --- a/src/components/Message/__tests__/MessageSimple.test.js +++ b/src/components/Message/__tests__/MessageSimple.test.js @@ -36,6 +36,7 @@ import { useMockedApis, } from '../../../mock-builders'; import { MessageBouncePrompt } from '../../MessageBounce'; +import { generateReminderResponse } from '../../../mock-builders/generator/reminder'; expect.extend(toHaveNoViolations); @@ -250,6 +251,28 @@ describe('', () => { expect(results).toHaveNoViolations(); }); + it('should render custom ReminderNotification component when one is given', async () => { + const message = generateAliceMessage({ reminder: generateReminderResponse() }); + client.reminders.hydrateState([message]); + const testId = 'custom-reminder-notification'; + const CustomReminderNotification = () =>
; + + const { container } = await renderMessageSimple({ + channelConfigOverrides: { + user_message_reminder: true, + }, + components: { + ReminderNotification: CustomReminderNotification, + }, + message, + }); + + expect(await screen.findByTestId(testId)).toBeInTheDocument(); + + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); + // FIXME: test relying on deprecated channel config parameter it('should render reaction list even though sending reactions is disabled in channel config', async () => { const reactions = [generateReaction({ user: bob })]; diff --git a/src/components/Message/index.ts b/src/components/Message/index.ts index 222b4a8bcf..5cdcce78d8 100644 --- a/src/components/Message/index.ts +++ b/src/components/Message/index.ts @@ -10,6 +10,7 @@ export * from './MessageStatus'; export * from './MessageText'; export * from './MessageTimestamp'; export * from './QuotedMessage'; +export * from './ReminderNotification'; export * from './renderText'; export * from './types'; export * from './utils'; diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index 9b34e728ae..79fbd3d556 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -37,6 +37,7 @@ import type { ReactionsListModalProps, ReactionsListProps, RecordingPermissionDeniedNotificationProps, + ReminderNotificationProps, SendButtonProps, StartRecordingAudioButtonProps, StreamedMessageTextProps, @@ -172,6 +173,7 @@ export type ComponentContextValue = { /** Custom UI component to display the reactions modal, defaults to and accepts same props as: [ReactionsListModal](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Reactions/ReactionsListModal.tsx) */ ReactionsListModal?: React.ComponentType; RecordingPermissionDeniedNotification?: React.ComponentType; + ReminderNotification?: React.ComponentType; /** Custom component to display the search UI, defaults to and accepts same props as: [Search](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Search/Search.tsx) */ Search?: React.ComponentType; /** Custom component to display the UI where the searched string is entered, defaults to and accepts same props as: [SearchBar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Search/SearchBar/SearchBar.tsx) */ diff --git a/src/mock-builders/generator/message.js b/src/mock-builders/generator/message.js index bc13785ec8..985003f0ad 100644 --- a/src/mock-builders/generator/message.js +++ b/src/mock-builders/generator/message.js @@ -1,17 +1,23 @@ import { nanoid } from 'nanoid'; -export const generateMessage = (options) => ({ - __html: '

regular

', - attachments: [], - created_at: new Date(), - html: '

regular

', - id: nanoid(), - mentioned_users: [], - pinned_at: null, - status: 'received', - text: nanoid(), - type: 'regular', - updated_at: new Date(), - user: null, - ...options, -}); +export const generateMessage = (options) => { + const data = { + __html: '

regular

', + attachments: [], + created_at: new Date(), + html: '

regular

', + id: nanoid(), + mentioned_users: [], + pinned_at: null, + status: 'received', + text: nanoid(), + type: 'regular', + updated_at: new Date(), + user: null, + ...options, + }; + if (data.reminder) { + data.reminder.message_id = data.id; + } + return data; +}; diff --git a/src/mock-builders/generator/reminder.ts b/src/mock-builders/generator/reminder.ts new file mode 100644 index 0000000000..6ab877a1d2 --- /dev/null +++ b/src/mock-builders/generator/reminder.ts @@ -0,0 +1,33 @@ +import type { ReminderResponse } from 'stream-chat'; + +const baseData = { + channel_cid: 'channel_cid', + message_id: 'message_id', + user_id: 'user_id', +} as const; + +export const generateReminderResponse = ({ + data, + scheduleOffsetMs, +}: { + data?: Partial; + scheduleOffsetMs?: number; +} = {}): ReminderResponse => { + const created_at = new Date().toISOString(); + const basePayload: ReminderResponse = { + ...baseData, + created_at, + message: { id: baseData.message_id, type: 'regular' }, + updated_at: created_at, + user: { id: baseData.user_id }, + }; + if (typeof scheduleOffsetMs === 'number') { + basePayload.remind_at = new Date( + new Date(created_at).getTime() + scheduleOffsetMs, + ).toISOString(); + } + return { + ...basePayload, + ...data, + }; +}; From de44853754000eb7bd63a5f8d7b2c44c25045ac7 Mon Sep 17 00:00:00 2001 From: martincupela Date: Tue, 3 Jun 2025 16:43:46 +0200 Subject: [PATCH 04/11] feat: display reminder deadline after the reminder stops to refresh --- .../Message/ReminderNotification.tsx | 32 ++++++++--- .../__tests__/ReminderNotification.test.js | 56 +++++++++++++++++++ .../ReminderNotification.test.js.snap | 49 ++++++++++++++++ src/i18n/de.json | 14 +++-- src/i18n/en.json | 2 + src/i18n/es.json | 14 +++-- src/i18n/fr.json | 14 +++-- src/i18n/hi.json | 14 +++-- src/i18n/it.json | 14 +++-- src/i18n/ja.json | 14 +++-- src/i18n/ko.json | 14 +++-- src/i18n/nl.json | 14 +++-- src/i18n/pt.json | 14 +++-- src/i18n/ru.json | 14 +++-- src/i18n/tr.json | 14 +++-- 15 files changed, 219 insertions(+), 74 deletions(-) create mode 100644 src/components/Message/__tests__/ReminderNotification.test.js create mode 100644 src/components/Message/__tests__/__snapshots__/ReminderNotification.test.js.snap diff --git a/src/components/Message/ReminderNotification.tsx b/src/components/Message/ReminderNotification.tsx index 90b311a6d1..37a085f977 100644 --- a/src/components/Message/ReminderNotification.tsx +++ b/src/components/Message/ReminderNotification.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { useTranslationContext } from '../../context'; +import React, { useMemo } from 'react'; +import { useChatContext, useTranslationContext } from '../../context'; import { useStateStore } from '../../store'; import type { Reminder, ReminderState } from 'stream-chat'; @@ -10,22 +10,38 @@ export type ReminderNotificationProps = { const reminderStateSelector = (state: ReminderState) => ({ timeLeftMs: state.timeLeftMs, }); + export const ReminderNotification = ({ reminder }: ReminderNotificationProps) => { + const { client } = useChatContext(); const { t } = useTranslationContext(); const { timeLeftMs } = useStateStore(reminder?.state, reminderStateSelector) ?? {}; + const isBehindRefreshBoundary = useMemo(() => { + const { stopRefreshBoundaryMs } = client.reminders.timers.config; + const stopRefreshTimeStamp = reminder?.remindAt + ? reminder?.remindAt.getTime() + stopRefreshBoundaryMs + : undefined; + return !!stopRefreshTimeStamp && new Date().getTime() > stopRefreshTimeStamp; + }, [client, reminder]); + return (

{t('Saved for later')} - {timeLeftMs !== null && ( + {reminder?.remindAt && timeLeftMs !== null && ( <> | - {t(`Due {{ dueTimeElapsed }}`, { - dueTimeElapsed: t('duration/Message reminder', { - milliseconds: timeLeftMs, - }), - })} + {isBehindRefreshBoundary + ? t('Due since {{ dueSince }}', { + dueSince: t(`timestamp/ReminderNotification`, { + timestamp: reminder.remindAt, + }), + }) + : t(`Due {{ dueTimeElapsed }}`, { + dueTimeElapsed: t('duration/Message reminder', { + milliseconds: timeLeftMs, + }), + })} )} diff --git a/src/components/Message/__tests__/ReminderNotification.test.js b/src/components/Message/__tests__/ReminderNotification.test.js new file mode 100644 index 0000000000..bbbfed2e70 --- /dev/null +++ b/src/components/Message/__tests__/ReminderNotification.test.js @@ -0,0 +1,56 @@ +import React from 'react'; +import { act, render } from '@testing-library/react'; +import { Chat } from '../../Chat'; +import { ReminderNotification } from '../ReminderNotification'; +import { generateUser, getTestClientWithUser } from '../../../mock-builders'; +import { generateReminderResponse } from '../../../mock-builders/generator/reminder'; + +const user = generateUser(); +const renderComponent = async ({ client, reminder }) => { + let result; + await act(() => { + result = render( + + + , + ); + }); + return result; +}; + +describe('ReminderNotification', () => { + it('displays text for bookmark notifications', async () => { + const client = await getTestClientWithUser(user); + const reminderResponse = generateReminderResponse(); + client.reminders.upsertToState({ + data: reminderResponse, + }); + const reminder = client.reminders.getFromState(reminderResponse.message_id); + const { container } = await renderComponent({ client, reminder }); + expect(container).toMatchSnapshot(); + }); + it('displays text for bookmark notifications and time due in case of timed reminders', async () => { + const client = await getTestClientWithUser(user); + const reminderResponse = generateReminderResponse({ + scheduleOffsetMs: 60 * 1000, + }); + client.reminders.upsertToState({ + data: reminderResponse, + }); + const reminder = client.reminders.getFromState(reminderResponse.message_id); + const { container } = await renderComponent({ client, reminder }); + expect(container).toMatchSnapshot(); + }); + it('displays text for bookmark notifications and reminder deadline if trespassed the refresh boundary', async () => { + const client = await getTestClientWithUser(user); + const reminderResponse = generateReminderResponse({ + data: { remind_at: new Date(0).toISOString() }, + }); + client.reminders.upsertToState({ + data: reminderResponse, + }); + const reminder = client.reminders.getFromState(reminderResponse.message_id); + const { container } = await renderComponent({ client, reminder }); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/components/Message/__tests__/__snapshots__/ReminderNotification.test.js.snap b/src/components/Message/__tests__/__snapshots__/ReminderNotification.test.js.snap new file mode 100644 index 0000000000..30b4e88efd --- /dev/null +++ b/src/components/Message/__tests__/__snapshots__/ReminderNotification.test.js.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ReminderNotification displays text for bookmark notifications 1`] = ` +

+

+ + Saved for later + +

+
+`; + +exports[`ReminderNotification displays text for bookmark notifications and reminder deadline if trespassed the refresh boundary 1`] = ` +
+

+ + Saved for later + + + | + + + Due since 01/01/1970 + +

+
+`; + +exports[`ReminderNotification displays text for bookmark notifications and time due in case of timed reminders 1`] = ` +
+

+ + Saved for later + + + | + + + Due in a minute + +

+
+`; diff --git a/src/i18n/de.json b/src/i18n/de.json index 5ea9016c3d..2e8554aad1 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -27,7 +27,8 @@ "Download attachment {{ name }}": "Anhang {{ name }} herunterladen", "Drag your files here": "Ziehen Sie Ihre Dateien hierher", "Drag your files here to add to your post": "Ziehen Sie Ihre Dateien hierher, um sie Ihrem Beitrag hinzuzufügen", - "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", + "Due since {{ dueSince }}": "Fällig seit {{ dueSince }}", + "Due {{ dueTimeElapsed }}": "Fällig {{ dueTimeElapsed }}", "Edit Message": "Nachricht bearbeiten", "Edit message request failed": "Anfrage zum Bearbeiten der Nachricht fehlgeschlagen", "Edited": "Bearbeitet", @@ -94,12 +95,12 @@ "Question": "Frage", "Quote": "Zitieren", "Recording format is not supported and cannot be reproduced": "Aufnahmeformat wird nicht unterstützt und kann nicht wiedergegeben werden", - "Remind Me": "Remind Me", - "Remove reminder": "Remove reminder", + "Remind Me": "Erinnern", + "Remove reminder": "Erinnerung entfernen", "Reply": "Antworten", "Reply to Message": "Auf Nachricht antworten", - "Save for later": "Save for later", - "Saved for later": "Saved for later", + "Save for later": "Für später speichern", + "Saved for later": "Für später gespeichert", "Search": "Suche", "Searching...": "Suchen...", "See all options ({{count}})_one": "Alle Optionen anzeigen ({{count}})", @@ -163,7 +164,7 @@ "aria/Open Reaction Selector": "Reaktionsauswahl öffnen", "aria/Open Thread": "Thread öffnen", "aria/Reaction list": "Reaktionsliste", - "aria/Remind Me Options": "aria/Remind Me Options", + "aria/Remind Me Options": "Erinnerungsoptionen", "aria/Remove attachment": "Anhang entfernen", "aria/Retry upload": "Upload erneut versuchen", "aria/Search results": "Suchergebnisse", @@ -190,6 +191,7 @@ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/PollVote": "{{ timestamp | timestampFormatter(format: MMM D [at] HH:mm) }}", "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", + "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "unban-command-args": "[@Benutzername]", "unban-command-description": "Einen Benutzer entbannen", diff --git a/src/i18n/en.json b/src/i18n/en.json index 2a5d241f47..39db0f7eb1 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -27,6 +27,7 @@ "Download attachment {{ name }}": "Download attachment {{ name }}", "Drag your files here": "Drag your files here", "Drag your files here to add to your post": "Drag your files here to add to your post", + "Due since {{ dueSince }}": "Due since {{ dueSince }}", "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", "Edit Message": "Edit Message", "Edit message request failed": "Edit message request failed", @@ -185,6 +186,7 @@ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/PollVote": "{{ timestamp | timestampFormatter(format: MMM D [at] HH:mm) }}", "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", + "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "unreadMessagesSeparatorText_one": "1 unread message", "unreadMessagesSeparatorText_other": "{{count}} unread messages", diff --git a/src/i18n/es.json b/src/i18n/es.json index 81fd5afb61..d057909ac4 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -27,7 +27,8 @@ "Download attachment {{ name }}": "Descargar adjunto {{ name }}", "Drag your files here": "Arrastra tus archivos aquí", "Drag your files here to add to your post": "Arrastra tus archivos aquí para agregarlos a tu publicación", - "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", + "Due since {{ dueSince }}": "Vencido desde {{ dueSince }}", + "Due {{ dueTimeElapsed }}": "Vencido {{ dueTimeElapsed }}", "Edit Message": "Editar mensaje", "Edit message request failed": "Error al editar la solicitud de mensaje", "Edited": "Editado", @@ -94,12 +95,12 @@ "Question": "Pregunta", "Quote": "Citar", "Recording format is not supported and cannot be reproduced": "El formato de grabación no es compatible y no se puede reproducir", - "Remind Me": "Remind Me", - "Remove reminder": "Remove reminder", + "Remind Me": "Recordarme", + "Remove reminder": "Eliminar recordatorio", "Reply": "Responder", "Reply to Message": "Responder al mensaje", - "Save for later": "Save for later", - "Saved for later": "Saved for later", + "Save for later": "Guardar para más tarde", + "Saved for later": "Guardado para más tarde", "Search": "Buscar", "Searching...": "Buscando...", "See all options ({{count}})_many": "Ver todas las opciones ({{count}})", @@ -166,7 +167,7 @@ "aria/Open Reaction Selector": "Abrir selector de reacciones", "aria/Open Thread": "Abrir hilo", "aria/Reaction list": "Lista de reacciones", - "aria/Remind Me Options": "aria/Remind Me Options", + "aria/Remind Me Options": "Opciones de recordatorio", "aria/Remove attachment": "Eliminar adjunto", "aria/Retry upload": "Reintentar carga", "aria/Search results": "Resultados de búsqueda", @@ -195,6 +196,7 @@ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/PollVote": "{{ timestamp | timestampFormatter(format: MMM D [at] HH:mm) }}", "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", + "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "unban-command-args": "[@usuario]", "unban-command-description": "Quitar la prohibición a un usuario", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 03d21b91e6..0f0143af49 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -27,7 +27,8 @@ "Download attachment {{ name }}": "Télécharger la pièce jointe {{ name }}", "Drag your files here": "Glissez vos fichiers ici", "Drag your files here to add to your post": "Glissez vos fichiers ici pour les ajouter à votre publication", - "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", + "Due since {{ dueSince }}": "Échéance depuis {{ dueSince }}", + "Due {{ dueTimeElapsed }}": "Échéance {{ dueTimeElapsed }}", "Edit Message": "Éditer un message", "Edit message request failed": "Échec de la demande de modification du message", "Edited": "Modifié", @@ -94,12 +95,12 @@ "Question": "Question", "Quote": "Citer", "Recording format is not supported and cannot be reproduced": "Le format d'enregistrement n'est pas pris en charge et ne peut pas être reproduit", - "Remind Me": "Remind Me", - "Remove reminder": "Remove reminder", + "Remind Me": "Me rappeler", + "Remove reminder": "Supprimer le rappel", "Reply": "Répondre", "Reply to Message": "Répondre au message", - "Save for later": "Save for later", - "Saved for later": "Saved for later", + "Save for later": "Enregistrer pour plus tard", + "Saved for later": "Enregistré pour plus tard", "Search": "Rechercher", "Searching...": "Recherche en cours...", "See all options ({{count}})_many": "Voir toutes les options ({{count}})", @@ -166,7 +167,7 @@ "aria/Open Reaction Selector": "Ouvrir le sélecteur de réactions", "aria/Open Thread": "Ouvrir le fil", "aria/Reaction list": "Liste des réactions", - "aria/Remind Me Options": "aria/Remind Me Options", + "aria/Remind Me Options": "Options de rappel", "aria/Remove attachment": "Supprimer la pièce jointe", "aria/Retry upload": "Réessayer le téléchargement", "aria/Search results": "Résultats de recherche", @@ -195,6 +196,7 @@ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/PollVote": "{{ timestamp | timestampFormatter(format: MMM D [at] HH:mm) }}", "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", + "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "unban-command-args": "[@nomdutilisateur]", "unban-command-description": "Débannir un utilisateur", diff --git a/src/i18n/hi.json b/src/i18n/hi.json index 2e551853c0..ac3a15cdbd 100644 --- a/src/i18n/hi.json +++ b/src/i18n/hi.json @@ -27,7 +27,8 @@ "Download attachment {{ name }}": "अनुलग्नक {{ name }} डाउनलोड करें", "Drag your files here": "अपनी फ़ाइलें यहाँ खींचें", "Drag your files here to add to your post": "अपनी फ़ाइलें यहाँ खींचें और अपने पोस्ट में जोड़ने के लिए", - "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", + "Due since {{ dueSince }}": "{{ dueSince }} से देय", + "Due {{ dueTimeElapsed }}": "{{ dueTimeElapsed }} में देय", "Edit Message": "मैसेज में बदलाव करे", "Edit message request failed": "संदेश संपादित करने का अनुरोध विफल रहा", "Edited": "संपादित", @@ -95,12 +96,12 @@ "Question": "प्रश्न", "Quote": "उद्धरण", "Recording format is not supported and cannot be reproduced": "रेकॉर्डिंग फ़ॉर्मेट समर्थित नहीं है और पुनः उत्पन्न नहीं किया जा सकता", - "Remind Me": "Remind Me", - "Remove reminder": "Remove reminder", + "Remind Me": "मुझे याद दिलाएं", + "Remove reminder": "रिमाइंडर हटाएं", "Reply": "जवाब दे दो", "Reply to Message": "संदेश का जवाब दें", - "Save for later": "Save for later", - "Saved for later": "Saved for later", + "Save for later": "बाद के लिए सहेजें", + "Saved for later": "बाद के लिए सहेजा गया", "Search": "खोज", "Searching...": "खोज कर...", "See all options ({{count}})_one": "सभी विकल्प देखें ({{count}})", @@ -164,7 +165,7 @@ "aria/Open Reaction Selector": "प्रतिक्रिया चयनकर्ता खोलें", "aria/Open Thread": "थ्रेड खोलें", "aria/Reaction list": "प्रतिक्रिया सूची", - "aria/Remind Me Options": "aria/Remind Me Options", + "aria/Remind Me Options": "रिमाइंडर विकल्प", "aria/Remove attachment": "संलग्नक हटाएं", "aria/Retry upload": "अपलोड पुनः प्रयास करें", "aria/Search results": "खोज परिणाम", @@ -191,6 +192,7 @@ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/PollVote": "{{ timestamp | timestampFormatter(format: MMM D [at] HH:mm) }}", "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", + "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "unban-command-args": "[@उपयोगकर्तनाम]", "unban-command-description": "एक उपयोगकर्ता को प्रतिषेध से मुक्त करें", diff --git a/src/i18n/it.json b/src/i18n/it.json index 96c2540862..e2965a2a08 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -27,7 +27,8 @@ "Download attachment {{ name }}": "Scarica l'allegato {{ name }}", "Drag your files here": "Trascina i tuoi file qui", "Drag your files here to add to your post": "Trascina i tuoi file qui per aggiungerli al tuo post", - "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", + "Due since {{ dueSince }}": "Scaduto dal {{ dueSince }}", + "Due {{ dueTimeElapsed }}": "Scadenza {{ dueTimeElapsed }}", "Edit Message": "Modifica messaggio", "Edit message request failed": "Richiesta di modifica del messaggio non riuscita", "Edited": "Modificato", @@ -94,12 +95,12 @@ "Question": "Domanda", "Quote": "Citazione", "Recording format is not supported and cannot be reproduced": "Il formato di registrazione non è supportato e non può essere riprodotto", - "Remind Me": "Remind Me", - "Remove reminder": "Remove reminder", + "Remind Me": "Ricordami", + "Remove reminder": "Rimuovi promemoria", "Reply": "Rispondi", "Reply to Message": "Rispondi al messaggio", - "Save for later": "Save for later", - "Saved for later": "Saved for later", + "Save for later": "Salva per dopo", + "Saved for later": "Salvato per dopo", "Search": "Cerca", "Searching...": "Ricerca in corso...", "See all options ({{count}})_many": "Vedi tutte le opzioni ({{count}})", @@ -166,7 +167,7 @@ "aria/Open Reaction Selector": "Apri il selettore di reazione", "aria/Open Thread": "Apri discussione", "aria/Reaction list": "Elenco delle reazioni", - "aria/Remind Me Options": "aria/Remind Me Options", + "aria/Remind Me Options": "Opzioni promemoria", "aria/Remove attachment": "Rimuovi allegato", "aria/Retry upload": "Riprova caricamento", "aria/Search results": "Risultati della ricerca", @@ -195,6 +196,7 @@ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/PollVote": "{{ timestamp | timestampFormatter(format: MMM D [at] HH:mm) }}", "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", + "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "unban-command-args": "[@nomeutente]", "unban-command-description": "Togliere il divieto a un utente", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index 9a44d0205f..d9a275d7fc 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -27,7 +27,8 @@ "Download attachment {{ name }}": "添付ファイル {{ name }} をダウンロード", "Drag your files here": "ここにファイルをドラッグ", "Drag your files here to add to your post": "投稿に追加するためにここにファイルをドラッグ", - "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", + "Due since {{ dueSince }}": "{{ dueSince }}から期限切れ", + "Due {{ dueTimeElapsed }}": "{{ dueTimeElapsed }}に期限切れ", "Edit Message": "メッセージを編集", "Edit message request failed": "メッセージの編集要求が失敗しました", "Edited": "編集済み", @@ -94,12 +95,12 @@ "Question": "質問", "Quote": "引用", "Recording format is not supported and cannot be reproduced": "録音形式はサポートされておらず、再生できません", - "Remind Me": "Remind Me", - "Remove reminder": "Remove reminder", + "Remind Me": "リマインダー", + "Remove reminder": "リマインダーを削除", "Reply": "返事", "Reply to Message": "メッセージに返信", - "Save for later": "Save for later", - "Saved for later": "Saved for later", + "Save for later": "後で保存", + "Saved for later": "後で保存済み", "Search": "探す", "Searching...": "検索中...", "See all options ({{count}})_other": "すべてのオプションを見る ({{count}})", @@ -160,7 +161,7 @@ "aria/Open Reaction Selector": "リアクションセレクターを開く", "aria/Open Thread": "スレッドを開く", "aria/Reaction list": "リアクション一覧", - "aria/Remind Me Options": "aria/Remind Me Options", + "aria/Remind Me Options": "リマインダーオプション", "aria/Remove attachment": "添付ファイルを削除", "aria/Retry upload": "アップロードを再試行", "aria/Search results": "検索結果", @@ -187,6 +188,7 @@ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/PollVote": "{{ timestamp | timestampFormatter(format: MMM D [at] HH:mm) }}", "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", + "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "unban-command-args": "[@ユーザ名]", "unban-command-description": "ユーザーの禁止を解除する", diff --git a/src/i18n/ko.json b/src/i18n/ko.json index f220fbc587..828f2b7f0a 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -27,7 +27,8 @@ "Download attachment {{ name }}": "첨부 파일 {{ name }} 다운로드", "Drag your files here": "여기로 파일을 끌어다 놓으세요", "Drag your files here to add to your post": "게시물에 추가하려면 파일을 여기로 끌어다 놓으세요", - "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", + "Due since {{ dueSince }}": "{{ dueSince }}부터 기한", + "Due {{ dueTimeElapsed }}": "{{ dueTimeElapsed }}에 기한", "Edit Message": "메시지 수정", "Edit message request failed": "메시지 수정 요청 실패", "Edited": "편집됨", @@ -94,12 +95,12 @@ "Question": "질문", "Quote": "인용", "Recording format is not supported and cannot be reproduced": "녹음 형식이 지원되지 않으므로 재생할 수 없습니다", - "Remind Me": "Remind Me", - "Remove reminder": "Remove reminder", + "Remind Me": "알림 설정", + "Remove reminder": "알림 제거", "Reply": "답장", "Reply to Message": "메시지에 답장", - "Save for later": "Save for later", - "Saved for later": "Saved for later", + "Save for later": "나중에 저장", + "Saved for later": "나중에 저장됨", "Search": "찾다", "Searching...": "수색...", "See all options ({{count}})_other": "모든 옵션 보기 ({{count}})", @@ -160,7 +161,7 @@ "aria/Open Reaction Selector": "반응 선택기 열기", "aria/Open Thread": "스레드 열기", "aria/Reaction list": "반응 목록", - "aria/Remind Me Options": "aria/Remind Me Options", + "aria/Remind Me Options": "알림 옵션", "aria/Remove attachment": "첨부 파일 제거", "aria/Retry upload": "업로드 다시 시도", "aria/Search results": "검색 결과", @@ -187,6 +188,7 @@ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/PollVote": "{{ timestamp | timestampFormatter(format: MMM D [at] HH:mm) }}", "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", + "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "unban-command-args": "[@사용자이름]", "unban-command-description": "사용자 차단 해제", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index 40ece59734..1f8b5cf37e 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -27,7 +27,8 @@ "Download attachment {{ name }}": "Bijlage {{ name }} downloaden", "Drag your files here": "Sleep je bestanden hier naartoe", "Drag your files here to add to your post": "Sleep je bestanden hier naartoe om aan je bericht toe te voegen", - "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", + "Due since {{ dueSince }}": "Vervallen sinds {{ dueSince }}", + "Due {{ dueTimeElapsed }}": "Vervallen {{ dueTimeElapsed }}", "Edit Message": "Bericht bewerken", "Edit message request failed": "Verzoek om bericht bewerken mislukt", "Edited": "Bewerkt", @@ -94,12 +95,12 @@ "Question": "Vraag", "Quote": "Citeer", "Recording format is not supported and cannot be reproduced": "Opnameformaat wordt niet ondersteund en kan niet worden gereproduceerd", - "Remind Me": "Remind Me", - "Remove reminder": "Remove reminder", + "Remind Me": "Herinner mij", + "Remove reminder": "Herinnering verwijderen", "Reply": "Antwoord", "Reply to Message": "Antwoord op bericht", - "Save for later": "Save for later", - "Saved for later": "Saved for later", + "Save for later": "Bewaren voor later", + "Saved for later": "Bewaard voor later", "Search": "Zoeken", "Searching...": "Zoeken...", "See all options ({{count}})_one": "Bekijk alle opties ({{count}})", @@ -163,7 +164,7 @@ "aria/Open Reaction Selector": "Reactiekiezer openen", "aria/Open Thread": "Draad openen", "aria/Reaction list": "Reactielijst", - "aria/Remind Me Options": "aria/Remind Me Options", + "aria/Remind Me Options": "Herinneringsopties", "aria/Remove attachment": "Bijlage verwijderen", "aria/Retry upload": "Upload opnieuw proberen", "aria/Search results": "Zoekresultaten", @@ -190,6 +191,7 @@ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/PollVote": "{{ timestamp | timestampFormatter(format: MMM D [at] HH:mm) }}", "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", + "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "unban-command-args": "[@gebruikersnaam]", "unban-command-description": "Een gebruiker debannen", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index 029bde527c..f54bee9166 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -27,7 +27,8 @@ "Download attachment {{ name }}": "Baixar anexo {{ name }}", "Drag your files here": "Arraste seus arquivos aqui", "Drag your files here to add to your post": "Arraste seus arquivos aqui para adicionar ao seu post", - "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", + "Due since {{ dueSince }}": "Vencido desde {{ dueSince }}", + "Due {{ dueTimeElapsed }}": "Vencido {{ dueTimeElapsed }}", "Edit Message": "Editar Mensagem", "Edit message request failed": "O pedido de edição da mensagem falhou", "Edited": "Editada", @@ -94,12 +95,12 @@ "Question": "Pergunta", "Quote": "Citar", "Recording format is not supported and cannot be reproduced": "Formato de gravação não é suportado e não pode ser reproduzido", - "Remind Me": "Remind Me", - "Remove reminder": "Remove reminder", + "Remind Me": "Lembrar-me", + "Remove reminder": "Remover lembrete", "Reply": "Responder", "Reply to Message": "Responder à mensagem", - "Save for later": "Save for later", - "Saved for later": "Saved for later", + "Save for later": "Salvar para depois", + "Saved for later": "Salvo para depois", "Search": "Buscar", "Searching...": "Buscando...", "See all options ({{count}})_many": "Ver todas as opções ({{count}})", @@ -166,7 +167,7 @@ "aria/Open Reaction Selector": "Abrir seletor de reações", "aria/Open Thread": "Abrir tópico", "aria/Reaction list": "Lista de reações", - "aria/Remind Me Options": "aria/Remind Me Options", + "aria/Remind Me Options": "Opções de lembrete", "aria/Remove attachment": "Remover anexo", "aria/Retry upload": "Tentar upload novamente", "aria/Search results": "Resultados da pesquisa", @@ -195,6 +196,7 @@ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/PollVote": "{{ timestamp | timestampFormatter(format: MMM D [at] HH:mm) }}", "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", + "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "unban-command-args": "[@nomedeusuário]", "unban-command-description": "Desbanir um usuário", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 35619b8f06..62a644734c 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -27,7 +27,8 @@ "Download attachment {{ name }}": "Скачать вложение {{ name }}", "Drag your files here": "Перетащите ваши файлы сюда", "Drag your files here to add to your post": "Перетащите ваши файлы сюда, чтобы добавить их в ваш пост", - "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", + "Due since {{ dueSince }}": "Просрочено с {{ dueSince }}", + "Due {{ dueTimeElapsed }}": "Просрочено {{ dueTimeElapsed }}", "Edit Message": "Редактировать сообщение", "Edit message request failed": "Не удалось изменить запрос сообщения", "Edited": "Отредактировано", @@ -94,12 +95,12 @@ "Question": "Вопрос", "Quote": "Цитировать", "Recording format is not supported and cannot be reproduced": "Формат записи не поддерживается и не может быть воспроизведен", - "Remind Me": "Remind Me", - "Remove reminder": "Remove reminder", + "Remind Me": "Напомнить мне", + "Remove reminder": "Удалить напоминание", "Reply": "Ответить", "Reply to Message": "Ответить на сообщение", - "Save for later": "Save for later", - "Saved for later": "Saved for later", + "Save for later": "Сохранить на потом", + "Saved for later": "Сохранено на потом", "Search": "Поиск", "Searching...": "Ищем...", "See all options ({{count}})_few": "Посмотреть все варианты ({{count}})", @@ -169,7 +170,7 @@ "aria/Open Reaction Selector": "Открыть селектор реакций", "aria/Open Thread": "Открыть тему", "aria/Reaction list": "Список реакций", - "aria/Remind Me Options": "aria/Remind Me Options", + "aria/Remind Me Options": "Параметры напоминания", "aria/Remove attachment": "Удалить вложение", "aria/Retry upload": "Повторить загрузку", "aria/Search results": "Результаты поиска", @@ -200,6 +201,7 @@ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/PollVote": "{{ timestamp | timestampFormatter(format: MMM D [at] HH:mm) }}", "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", + "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "unban-command-args": "[@имяпользователя]", "unban-command-description": "Разблокировать пользователя", diff --git a/src/i18n/tr.json b/src/i18n/tr.json index 652ff5fc65..cc24fe019c 100644 --- a/src/i18n/tr.json +++ b/src/i18n/tr.json @@ -27,7 +27,8 @@ "Download attachment {{ name }}": "Ek {{ name }}'i indir", "Drag your files here": "Dosyalarınızı buraya sürükleyin", "Drag your files here to add to your post": "Gönderinize eklemek için dosyalarınızı buraya sürükleyin", - "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", + "Due since {{ dueSince }}": "{{ dueSince }}'den beri süresi dolmuş", + "Due {{ dueTimeElapsed }}": "{{ dueTimeElapsed }}'de süresi dolacak", "Edit Message": "Mesajı Düzenle", "Edit message request failed": "Mesaj düzenleme isteği başarısız oldu", "Edited": "Düzenlendi", @@ -94,12 +95,12 @@ "Question": "Soru", "Quote": "Alıntı", "Recording format is not supported and cannot be reproduced": "Kayıt formatı desteklenmiyor ve çoğaltılamıyor", - "Remind Me": "Remind Me", - "Remove reminder": "Remove reminder", + "Remind Me": "Hatırlat", + "Remove reminder": "Hatırlatıcıyı kaldır", "Reply": "Cevapla", "Reply to Message": "Mesaja Cevapla", - "Save for later": "Save for later", - "Saved for later": "Saved for later", + "Save for later": "Daha sonra kaydet", + "Saved for later": "Daha sonra kaydedildi", "Search": "Arama", "Searching...": "Aranıyor...", "See all options ({{count}})_one": "Tüm seçenekleri göster ({{count}})", @@ -163,7 +164,7 @@ "aria/Open Reaction Selector": "Tepki Seçiciyi Aç", "aria/Open Thread": "Konuyu Aç", "aria/Reaction list": "Tepki listesi", - "aria/Remind Me Options": "aria/Remind Me Options", + "aria/Remind Me Options": "Hatırlatma seçenekleri", "aria/Remove attachment": "Eki kaldır", "aria/Retry upload": "Yüklemeyi Tekrar Dene", "aria/Search results": "Arama sonuçları", @@ -190,6 +191,7 @@ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/PollVote": "{{ timestamp | timestampFormatter(format: MMM D [at] HH:mm) }}", "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", + "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", "unban-command-args": "[@kullanıcıadı]", "unban-command-description": "Bir kullanıcının yasağını kaldır", From 7ddc0ee5d1c2a78f5a9ec24e6f631baf03b648b7 Mon Sep 17 00:00:00 2001 From: martincupela Date: Wed, 4 Jun 2025 14:45:17 +0200 Subject: [PATCH 05/11] refactor: adapt code to LLC changes --- src/components/Message/ReminderNotification.tsx | 9 +++++---- src/components/MessageActions/RemindMeSubmenu.tsx | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/Message/ReminderNotification.tsx b/src/components/Message/ReminderNotification.tsx index 37a085f977..8be1c23594 100644 --- a/src/components/Message/ReminderNotification.tsx +++ b/src/components/Message/ReminderNotification.tsx @@ -17,10 +17,11 @@ export const ReminderNotification = ({ reminder }: ReminderNotificationProps) => const { timeLeftMs } = useStateStore(reminder?.state, reminderStateSelector) ?? {}; const isBehindRefreshBoundary = useMemo(() => { - const { stopRefreshBoundaryMs } = client.reminders.timers.config; - const stopRefreshTimeStamp = reminder?.remindAt - ? reminder?.remindAt.getTime() + stopRefreshBoundaryMs - : undefined; + const stopRefreshBoundaryMs = client.reminders.stopTimerRefreshBoundaryMs; + const stopRefreshTimeStamp = + reminder?.remindAt && stopRefreshBoundaryMs + ? reminder?.remindAt.getTime() + stopRefreshBoundaryMs + : undefined; return !!stopRefreshTimeStamp && new Date().getTime() > stopRefreshTimeStamp; }, [client, reminder]); diff --git a/src/components/MessageActions/RemindMeSubmenu.tsx b/src/components/MessageActions/RemindMeSubmenu.tsx index 1225556362..0f146b175e 100644 --- a/src/components/MessageActions/RemindMeSubmenu.tsx +++ b/src/components/MessageActions/RemindMeSubmenu.tsx @@ -36,7 +36,7 @@ export const RemindMeSubmenu = () => { className='str-chat__message-actions-list-item-button' key={`reminder-offset-option--${offsetMs}`} onClick={() => { - client.reminders.createOrUpdateReminder({ + client.reminders.upsertReminder({ messageId: message.id, remind_at: new Date(new Date().getTime() + offsetMs).toISOString(), }); From c0d4a4be16e38f935588bdfa70513fa85c61d528 Mon Sep 17 00:00:00 2001 From: martincupela Date: Thu, 5 Jun 2025 12:48:34 +0200 Subject: [PATCH 06/11] chore(deps): upgrade stream-chat-js to version 9.5.0 --- package.json | 4 ++-- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 8fe6150c44..7404a56ec3 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "emoji-mart": "^5.4.0", "react": "^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.14.0", "react-dom": "^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.14.0", - "stream-chat": "^9.0.0" + "stream-chat": "^9.5.0" }, "peerDependenciesMeta": { "@breezystack/lamejs": { @@ -239,7 +239,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "semantic-release": "^24.2.3", - "stream-chat": "9.1.1", + "stream-chat": "^9.5.0", "ts-jest": "^29.2.5", "typescript": "^5.4.5", "typescript-eslint": "^8.17.0" diff --git a/yarn.lock b/yarn.lock index 6b5990b9be..72f8cf7c2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12155,10 +12155,10 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== -stream-chat@9.1.1: - version "9.1.1" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.1.1.tgz#c81ffa84a7ca579d9812065bc159010191b59090" - integrity sha512-7Y23aIVQMppNZgRj/rTFwIx9pszxgDcS99idkSXJSgdV8C7FlyDtiF1yQSdP0oiNFAt7OUP/xSqmbJTljrm24Q== +stream-chat@^9.5.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.5.0.tgz#163f42ff28458451051a4d13727055b3875292a9" + integrity sha512-cgtZrOjjoZDQAU4tvYoBSlLPPsYtEm8iDJSEt+q6uwS7Ln3HYLnKHovROovDSR324VVE+mBQ8hsCALS/pvBu2A== dependencies: "@types/jsonwebtoken" "^9.0.8" "@types/ws" "^8.5.14" From 2482861a52138d4cfd1e53e2030864081e826c99 Mon Sep 17 00:00:00 2001 From: martincupela Date: Wed, 11 Jun 2025 09:41:50 +0200 Subject: [PATCH 07/11] chore(deps): upgrade @stream-io/stream-chat-css to version 5.10.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 7404a56ec3..132d701bbf 100644 --- a/package.json +++ b/package.json @@ -186,7 +186,7 @@ "@playwright/test": "^1.42.1", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", - "@stream-io/stream-chat-css": "^5.9.1", + "@stream-io/stream-chat-css": "^5.10.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", diff --git a/yarn.lock b/yarn.lock index 72f8cf7c2a..6dc32646f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2438,10 +2438,10 @@ resolved "https://registry.yarnpkg.com/@stream-io/escape-string-regexp/-/escape-string-regexp-5.0.1.tgz#362505c92799fea6afe4e369993fbbda8690cc37" integrity sha512-qIaSrzJXieZqo2fZSYTdzwSbZgHHsT3tkd646vvZhh4fr+9nO4NlvqGmPF43Y+OfZiWf+zYDFgNiPGG5+iZulQ== -"@stream-io/stream-chat-css@^5.9.1": - version "5.9.1" - resolved "https://registry.yarnpkg.com/@stream-io/stream-chat-css/-/stream-chat-css-5.9.1.tgz#e551f4131d52dd96b8d3031aa3c4ed9ec151adae" - integrity sha512-BdgiE1ovsRdN1uVBDB9v2lF4woHMUi4cunrwLplLDdBkmWmoX51/59q4CYFk+Il5AGESVqE4HiFQ8doycUHL6g== +"@stream-io/stream-chat-css@^5.10.0": + version "5.10.0" + resolved "https://registry.yarnpkg.com/@stream-io/stream-chat-css/-/stream-chat-css-5.10.0.tgz#e144345b5bd79b5012061fcd93c79f9cae8f7e5e" + integrity sha512-FA4Slik47XsTGWvzBZHQ2badvmxGYNFnRWg3L6Ec7FTv8MPD4k9C0zMuT5DOJG2TdNHv/KPENJMF8VwudMdRnQ== "@stream-io/transliterate@^1.5.5": version "1.5.5" From 05ec4534865232997200c9b472dc402740b2db3a Mon Sep 17 00:00:00 2001 From: martincupela Date: Wed, 11 Jun 2025 10:26:10 +0200 Subject: [PATCH 08/11] refactor: change translation dueTimeElapsed to timeLeft --- .../Message/ReminderNotification.tsx | 4 +-- .../__tests__/ReminderNotification.test.js | 4 +-- .../ReminderNotification.test.js.snap | 36 +++++++++++++++++++ src/i18n/de.json | 2 +- src/i18n/en.json | 2 +- src/i18n/es.json | 2 +- src/i18n/fr.json | 2 +- src/i18n/hi.json | 2 +- src/i18n/it.json | 2 +- src/i18n/ja.json | 2 +- src/i18n/ko.json | 2 +- src/i18n/nl.json | 2 +- src/i18n/pt.json | 2 +- src/i18n/ru.json | 2 +- src/i18n/tr.json | 2 +- 15 files changed, 52 insertions(+), 16 deletions(-) diff --git a/src/components/Message/ReminderNotification.tsx b/src/components/Message/ReminderNotification.tsx index 8be1c23594..179113ded9 100644 --- a/src/components/Message/ReminderNotification.tsx +++ b/src/components/Message/ReminderNotification.tsx @@ -38,8 +38,8 @@ export const ReminderNotification = ({ reminder }: ReminderNotificationProps) => timestamp: reminder.remindAt, }), }) - : t(`Due {{ dueTimeElapsed }}`, { - dueTimeElapsed: t('duration/Message reminder', { + : t(`Due {{ timeLeft }}`, { + timeLeft: t('duration/Message reminder', { milliseconds: timeLeftMs, }), })} diff --git a/src/components/Message/__tests__/ReminderNotification.test.js b/src/components/Message/__tests__/ReminderNotification.test.js index bbbfed2e70..9940a58f9a 100644 --- a/src/components/Message/__tests__/ReminderNotification.test.js +++ b/src/components/Message/__tests__/ReminderNotification.test.js @@ -29,7 +29,7 @@ describe('ReminderNotification', () => { const { container } = await renderComponent({ client, reminder }); expect(container).toMatchSnapshot(); }); - it('displays text for bookmark notifications and time due in case of timed reminders', async () => { + it('displays text for time due in case of timed reminders', async () => { const client = await getTestClientWithUser(user); const reminderResponse = generateReminderResponse({ scheduleOffsetMs: 60 * 1000, @@ -41,7 +41,7 @@ describe('ReminderNotification', () => { const { container } = await renderComponent({ client, reminder }); expect(container).toMatchSnapshot(); }); - it('displays text for bookmark notifications and reminder deadline if trespassed the refresh boundary', async () => { + it('displays text for reminder deadline if trespassed the refresh boundary', async () => { const client = await getTestClientWithUser(user); const reminderResponse = generateReminderResponse({ data: { remind_at: new Date(0).toISOString() }, diff --git a/src/components/Message/__tests__/__snapshots__/ReminderNotification.test.js.snap b/src/components/Message/__tests__/__snapshots__/ReminderNotification.test.js.snap index 30b4e88efd..b35786f40c 100644 --- a/src/components/Message/__tests__/__snapshots__/ReminderNotification.test.js.snap +++ b/src/components/Message/__tests__/__snapshots__/ReminderNotification.test.js.snap @@ -47,3 +47,39 @@ exports[`ReminderNotification displays text for bookmark notifications and time

`; + +exports[`ReminderNotification displays text for reminder deadline if trespassed the refresh boundary 1`] = ` +
+

+ + Saved for later + + + | + + + Due 55 years ago + +

+
+`; + +exports[`ReminderNotification displays text for time due in case of timed reminders 1`] = ` +
+

+ + Saved for later + + + | + + + Due in a minute + +

+
+`; diff --git a/src/i18n/de.json b/src/i18n/de.json index 2e8554aad1..309d83a042 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -28,7 +28,7 @@ "Drag your files here": "Ziehen Sie Ihre Dateien hierher", "Drag your files here to add to your post": "Ziehen Sie Ihre Dateien hierher, um sie Ihrem Beitrag hinzuzufügen", "Due since {{ dueSince }}": "Fällig seit {{ dueSince }}", - "Due {{ dueTimeElapsed }}": "Fällig {{ dueTimeElapsed }}", + "Due {{ timeLeft }}": "Fällig {{ timeLeft }}", "Edit Message": "Nachricht bearbeiten", "Edit message request failed": "Anfrage zum Bearbeiten der Nachricht fehlgeschlagen", "Edited": "Bearbeitet", diff --git a/src/i18n/en.json b/src/i18n/en.json index 39db0f7eb1..519956ef6a 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -28,7 +28,7 @@ "Drag your files here": "Drag your files here", "Drag your files here to add to your post": "Drag your files here to add to your post", "Due since {{ dueSince }}": "Due since {{ dueSince }}", - "Due {{ dueTimeElapsed }}": "Due {{ dueTimeElapsed }}", + "Due {{ timeLeft }}": "Due {{ timeLeft }}", "Edit Message": "Edit Message", "Edit message request failed": "Edit message request failed", "Edited": "Edited", diff --git a/src/i18n/es.json b/src/i18n/es.json index d057909ac4..c2df7c872e 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -28,7 +28,7 @@ "Drag your files here": "Arrastra tus archivos aquí", "Drag your files here to add to your post": "Arrastra tus archivos aquí para agregarlos a tu publicación", "Due since {{ dueSince }}": "Vencido desde {{ dueSince }}", - "Due {{ dueTimeElapsed }}": "Vencido {{ dueTimeElapsed }}", + "Due {{ timeLeft }}": "Vence en {{ timeLeft }}", "Edit Message": "Editar mensaje", "Edit message request failed": "Error al editar la solicitud de mensaje", "Edited": "Editado", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 0f0143af49..7cc19a6fac 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -28,7 +28,7 @@ "Drag your files here": "Glissez vos fichiers ici", "Drag your files here to add to your post": "Glissez vos fichiers ici pour les ajouter à votre publication", "Due since {{ dueSince }}": "Échéance depuis {{ dueSince }}", - "Due {{ dueTimeElapsed }}": "Échéance {{ dueTimeElapsed }}", + "Due {{ timeLeft }}": "Échéance dans {{ timeLeft }}", "Edit Message": "Éditer un message", "Edit message request failed": "Échec de la demande de modification du message", "Edited": "Modifié", diff --git a/src/i18n/hi.json b/src/i18n/hi.json index ac3a15cdbd..e7b3e7c160 100644 --- a/src/i18n/hi.json +++ b/src/i18n/hi.json @@ -28,7 +28,7 @@ "Drag your files here": "अपनी फ़ाइलें यहाँ खींचें", "Drag your files here to add to your post": "अपनी फ़ाइलें यहाँ खींचें और अपने पोस्ट में जोड़ने के लिए", "Due since {{ dueSince }}": "{{ dueSince }} से देय", - "Due {{ dueTimeElapsed }}": "{{ dueTimeElapsed }} में देय", + "Due {{ timeLeft }}": "{{ timeLeft }} में देय", "Edit Message": "मैसेज में बदलाव करे", "Edit message request failed": "संदेश संपादित करने का अनुरोध विफल रहा", "Edited": "संपादित", diff --git a/src/i18n/it.json b/src/i18n/it.json index e2965a2a08..ceca2eff51 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -28,7 +28,7 @@ "Drag your files here": "Trascina i tuoi file qui", "Drag your files here to add to your post": "Trascina i tuoi file qui per aggiungerli al tuo post", "Due since {{ dueSince }}": "Scaduto dal {{ dueSince }}", - "Due {{ dueTimeElapsed }}": "Scadenza {{ dueTimeElapsed }}", + "Due {{ timeLeft }}": "Scadenza tra {{ timeLeft }}", "Edit Message": "Modifica messaggio", "Edit message request failed": "Richiesta di modifica del messaggio non riuscita", "Edited": "Modificato", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index d9a275d7fc..ad75ab9ce1 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -28,7 +28,7 @@ "Drag your files here": "ここにファイルをドラッグ", "Drag your files here to add to your post": "投稿に追加するためにここにファイルをドラッグ", "Due since {{ dueSince }}": "{{ dueSince }}から期限切れ", - "Due {{ dueTimeElapsed }}": "{{ dueTimeElapsed }}に期限切れ", + "Due {{ timeLeft }}": "{{ timeLeft }}に期限切れ", "Edit Message": "メッセージを編集", "Edit message request failed": "メッセージの編集要求が失敗しました", "Edited": "編集済み", diff --git a/src/i18n/ko.json b/src/i18n/ko.json index 828f2b7f0a..6597ffd32f 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -28,7 +28,7 @@ "Drag your files here": "여기로 파일을 끌어다 놓으세요", "Drag your files here to add to your post": "게시물에 추가하려면 파일을 여기로 끌어다 놓으세요", "Due since {{ dueSince }}": "{{ dueSince }}부터 기한", - "Due {{ dueTimeElapsed }}": "{{ dueTimeElapsed }}에 기한", + "Due {{ timeLeft }}": "{{ timeLeft }}에 기한", "Edit Message": "메시지 수정", "Edit message request failed": "메시지 수정 요청 실패", "Edited": "편집됨", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index 1f8b5cf37e..d6ed0ebc32 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -28,7 +28,7 @@ "Drag your files here": "Sleep je bestanden hier naartoe", "Drag your files here to add to your post": "Sleep je bestanden hier naartoe om aan je bericht toe te voegen", "Due since {{ dueSince }}": "Vervallen sinds {{ dueSince }}", - "Due {{ dueTimeElapsed }}": "Vervallen {{ dueTimeElapsed }}", + "Due {{ timeLeft }}": "Vervallen in {{ timeLeft }}", "Edit Message": "Bericht bewerken", "Edit message request failed": "Verzoek om bericht bewerken mislukt", "Edited": "Bewerkt", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index f54bee9166..a8122c0ab8 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -28,7 +28,7 @@ "Drag your files here": "Arraste seus arquivos aqui", "Drag your files here to add to your post": "Arraste seus arquivos aqui para adicionar ao seu post", "Due since {{ dueSince }}": "Vencido desde {{ dueSince }}", - "Due {{ dueTimeElapsed }}": "Vencido {{ dueTimeElapsed }}", + "Due {{ timeLeft }}": "Vence em {{ timeLeft }}", "Edit Message": "Editar Mensagem", "Edit message request failed": "O pedido de edição da mensagem falhou", "Edited": "Editada", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 62a644734c..514602e774 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -28,7 +28,7 @@ "Drag your files here": "Перетащите ваши файлы сюда", "Drag your files here to add to your post": "Перетащите ваши файлы сюда, чтобы добавить их в ваш пост", "Due since {{ dueSince }}": "Просрочено с {{ dueSince }}", - "Due {{ dueTimeElapsed }}": "Просрочено {{ dueTimeElapsed }}", + "Due {{ timeLeft }}": "Просрочено в {{ timeLeft }}", "Edit Message": "Редактировать сообщение", "Edit message request failed": "Не удалось изменить запрос сообщения", "Edited": "Отредактировано", diff --git a/src/i18n/tr.json b/src/i18n/tr.json index cc24fe019c..4940cf695c 100644 --- a/src/i18n/tr.json +++ b/src/i18n/tr.json @@ -28,7 +28,7 @@ "Drag your files here": "Dosyalarınızı buraya sürükleyin", "Drag your files here to add to your post": "Gönderinize eklemek için dosyalarınızı buraya sürükleyin", "Due since {{ dueSince }}": "{{ dueSince }}'den beri süresi dolmuş", - "Due {{ dueTimeElapsed }}": "{{ dueTimeElapsed }}'de süresi dolacak", + "Due {{ timeLeft }}": "{{ timeLeft }} içinde süresi dolacak", "Edit Message": "Mesajı Düzenle", "Edit message request failed": "Mesaj düzenleme isteği başarısız oldu", "Edited": "Düzenlendi", From 2f0aa244f129f7a405942f99906e8e59e337ddf6 Mon Sep 17 00:00:00 2001 From: martincupela Date: Wed, 11 Jun 2025 10:34:23 +0200 Subject: [PATCH 09/11] fix: prevent unnecessary memoization of isBehindRefreshBoundary in ReminderNotification --- .../Message/ReminderNotification.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/Message/ReminderNotification.tsx b/src/components/Message/ReminderNotification.tsx index 179113ded9..27fe56eb59 100644 --- a/src/components/Message/ReminderNotification.tsx +++ b/src/components/Message/ReminderNotification.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { useChatContext, useTranslationContext } from '../../context'; import { useStateStore } from '../../store'; import type { Reminder, ReminderState } from 'stream-chat'; @@ -16,14 +16,14 @@ export const ReminderNotification = ({ reminder }: ReminderNotificationProps) => const { t } = useTranslationContext(); const { timeLeftMs } = useStateStore(reminder?.state, reminderStateSelector) ?? {}; - const isBehindRefreshBoundary = useMemo(() => { - const stopRefreshBoundaryMs = client.reminders.stopTimerRefreshBoundaryMs; - const stopRefreshTimeStamp = - reminder?.remindAt && stopRefreshBoundaryMs - ? reminder?.remindAt.getTime() + stopRefreshBoundaryMs - : undefined; - return !!stopRefreshTimeStamp && new Date().getTime() > stopRefreshTimeStamp; - }, [client, reminder]); + const stopRefreshBoundaryMs = client.reminders.stopTimerRefreshBoundaryMs; + const stopRefreshTimeStamp = + reminder?.remindAt && stopRefreshBoundaryMs + ? reminder?.remindAt.getTime() + stopRefreshBoundaryMs + : undefined; + + const isBehindRefreshBoundary = + !!stopRefreshTimeStamp && new Date().getTime() > stopRefreshTimeStamp; return (

From e1c0b9eefafc18b9cd5e27513f8da480739913b2 Mon Sep 17 00:00:00 2001 From: martincupela Date: Wed, 11 Jun 2025 11:27:32 +0200 Subject: [PATCH 10/11] fix: calculate stopRefreshBoundaryMs in ReminderNotification from reminder timer --- .../Message/ReminderNotification.tsx | 5 +-- .../__tests__/ReminderNotification.test.js | 39 +++++++------------ .../ReminderNotification.test.js.snap | 38 +----------------- 3 files changed, 18 insertions(+), 64 deletions(-) diff --git a/src/components/Message/ReminderNotification.tsx b/src/components/Message/ReminderNotification.tsx index 27fe56eb59..e46d06a89f 100644 --- a/src/components/Message/ReminderNotification.tsx +++ b/src/components/Message/ReminderNotification.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useChatContext, useTranslationContext } from '../../context'; +import { useTranslationContext } from '../../context'; import { useStateStore } from '../../store'; import type { Reminder, ReminderState } from 'stream-chat'; @@ -12,11 +12,10 @@ const reminderStateSelector = (state: ReminderState) => ({ }); export const ReminderNotification = ({ reminder }: ReminderNotificationProps) => { - const { client } = useChatContext(); const { t } = useTranslationContext(); const { timeLeftMs } = useStateStore(reminder?.state, reminderStateSelector) ?? {}; - const stopRefreshBoundaryMs = client.reminders.stopTimerRefreshBoundaryMs; + const stopRefreshBoundaryMs = reminder?.timer.stopRefreshBoundaryMs; const stopRefreshTimeStamp = reminder?.remindAt && stopRefreshBoundaryMs ? reminder?.remindAt.getTime() + stopRefreshBoundaryMs diff --git a/src/components/Message/__tests__/ReminderNotification.test.js b/src/components/Message/__tests__/ReminderNotification.test.js index 9940a58f9a..efc0037357 100644 --- a/src/components/Message/__tests__/ReminderNotification.test.js +++ b/src/components/Message/__tests__/ReminderNotification.test.js @@ -1,4 +1,5 @@ import React from 'react'; +import { Reminder } from 'stream-chat'; import { act, render } from '@testing-library/react'; import { Chat } from '../../Chat'; import { ReminderNotification } from '../ReminderNotification'; @@ -6,7 +7,8 @@ import { generateUser, getTestClientWithUser } from '../../../mock-builders'; import { generateReminderResponse } from '../../../mock-builders/generator/reminder'; const user = generateUser(); -const renderComponent = async ({ client, reminder }) => { +const renderComponent = async ({ reminder }) => { + const client = await getTestClientWithUser(user); let result; await act(() => { result = render( @@ -20,37 +22,26 @@ const renderComponent = async ({ client, reminder }) => { describe('ReminderNotification', () => { it('displays text for bookmark notifications', async () => { - const client = await getTestClientWithUser(user); - const reminderResponse = generateReminderResponse(); - client.reminders.upsertToState({ - data: reminderResponse, - }); - const reminder = client.reminders.getFromState(reminderResponse.message_id); - const { container } = await renderComponent({ client, reminder }); + const reminder = new Reminder({ data: generateReminderResponse() }); + const { container } = await renderComponent({ reminder }); expect(container).toMatchSnapshot(); }); it('displays text for time due in case of timed reminders', async () => { - const client = await getTestClientWithUser(user); - const reminderResponse = generateReminderResponse({ - scheduleOffsetMs: 60 * 1000, - }); - client.reminders.upsertToState({ - data: reminderResponse, + const reminder = new Reminder({ + data: generateReminderResponse({ + scheduleOffsetMs: 60 * 1000, + }), }); - const reminder = client.reminders.getFromState(reminderResponse.message_id); - const { container } = await renderComponent({ client, reminder }); + const { container } = await renderComponent({ reminder }); expect(container).toMatchSnapshot(); }); it('displays text for reminder deadline if trespassed the refresh boundary', async () => { - const client = await getTestClientWithUser(user); - const reminderResponse = generateReminderResponse({ - data: { remind_at: new Date(0).toISOString() }, - }); - client.reminders.upsertToState({ - data: reminderResponse, + const reminder = new Reminder({ + data: generateReminderResponse({ + data: { remind_at: new Date(0).toISOString() }, + }), }); - const reminder = client.reminders.getFromState(reminderResponse.message_id); - const { container } = await renderComponent({ client, reminder }); + const { container } = await renderComponent({ reminder }); expect(container).toMatchSnapshot(); }); }); diff --git a/src/components/Message/__tests__/__snapshots__/ReminderNotification.test.js.snap b/src/components/Message/__tests__/__snapshots__/ReminderNotification.test.js.snap index b35786f40c..492cbc1c94 100644 --- a/src/components/Message/__tests__/__snapshots__/ReminderNotification.test.js.snap +++ b/src/components/Message/__tests__/__snapshots__/ReminderNotification.test.js.snap @@ -12,42 +12,6 @@ exports[`ReminderNotification displays text for bookmark notifications 1`] = ` `; -exports[`ReminderNotification displays text for bookmark notifications and reminder deadline if trespassed the refresh boundary 1`] = ` -

-

- - Saved for later - - - | - - - Due since 01/01/1970 - -

-
-`; - -exports[`ReminderNotification displays text for bookmark notifications and time due in case of timed reminders 1`] = ` -
-

- - Saved for later - - - | - - - Due in a minute - -

-
-`; - exports[`ReminderNotification displays text for reminder deadline if trespassed the refresh boundary 1`] = `

- Due 55 years ago + Due since 01/01/1970

From ab5d8574a0aad66b0bd5dd7cf425707f6b17ada3 Mon Sep 17 00:00:00 2001 From: martincupela Date: Wed, 11 Jun 2025 11:28:36 +0200 Subject: [PATCH 11/11] test: reflect removal of mutes from a user object in localMessage when sending a message --- .../MessageInput/__tests__/EditMessageForm.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/MessageInput/__tests__/EditMessageForm.test.js b/src/components/MessageInput/__tests__/EditMessageForm.test.js index 30ac900b61..b1afbce51e 100644 --- a/src/components/MessageInput/__tests__/EditMessageForm.test.js +++ b/src/components/MessageInput/__tests__/EditMessageForm.test.js @@ -1177,6 +1177,9 @@ describe(`EditMessageForm`, () => { await act(() => submit()); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { mutes, ...userWithoutMutes } = mainListMessage.user; + expect(editMock.mock.calls[1]).toEqual([ customChannel.cid, expect.objectContaining({ @@ -1199,6 +1202,8 @@ describe(`EditMessageForm`, () => { quoted_message: null, reaction_groups: null, text: '@mention-name ', + user: userWithoutMutes, + user_id: customClient.user.id, }), {}, ]);