+ {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: '