From 0cdf97d92489e60ab9c187d878850c98dabc785a Mon Sep 17 00:00:00 2001 From: "lingohub[bot]" <69908207+lingohub[bot]@users.noreply.github.com> Date: Wed, 11 Oct 2023 20:59:43 +0000 Subject: [PATCH 01/38] =?UTF-8?q?i18n:=20Language=20update=20from=20LingoH?= =?UTF-8?q?ub=20=F0=9F=A4=96=20on=202023-10-10Z=20(#30613)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com> --- apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json | 8 -------- apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json | 4 ---- apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json | 4 ---- apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json | 4 ---- apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json | 10 +--------- apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json | 2 +- apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json | 8 -------- apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json | 8 -------- apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json | 8 -------- apps/meteor/packages/rocketchat-i18n/i18n/gl.i18n.json | 4 ---- apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json | 8 -------- apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json | 8 -------- .../packages/rocketchat-i18n/i18n/ka-GE.i18n.json | 4 ---- apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json | 4 ---- apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json | 8 -------- apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json | 8 -------- .../packages/rocketchat-i18n/i18n/pt-BR.i18n.json | 8 -------- apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json | 8 -------- apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json | 8 -------- .../packages/rocketchat-i18n/i18n/zh-TW.i18n.json | 8 -------- apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json | 4 ---- 21 files changed, 2 insertions(+), 134 deletions(-) diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json index d6b7e5a0739b..392b4a99b3ad 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json @@ -4299,10 +4299,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "يمنع هذا الإعداد كل المثيلات من إرسال تغييرات الحالة للمستخدمين إلى عملائهم مع الاحتفاظ بحالة تواجد كل المستخدمين من التحميل الأول!", "Troubleshoot_Disable_Sessions_Monitor": "تعطيل شاشة مراقبة الجلسات", "Troubleshoot_Disable_Sessions_Monitor_Alert": "يوقف هذا الإعداد معالجة جلسات المستخدم، ما يتسبب في توقف الإحصاءات عن العمل بشكل صحيح!", - "Troubleshoot_Disable_Statistics_Generator": "تعطيل منشئ الإحصاءات", - "Troubleshoot_Disable_Statistics_Generator_Alert": "يوقف هذا الإعداد معالجة كل الإحصاءات، ما يجعل صفحة المعلومات قديمة حتى ينقر شخص ما على زر التحديث وقد يتسبب في فقد معلومات أخرى حول النظام!", - "Troubleshoot_Disable_Workspace_Sync": "تعطيل مزامنة مساحة العمل", - "Troubleshoot_Disable_Workspace_Sync_Alert": "يوقف هذا الإعداد مزامنة هذا الخادم مع سحابة Rocket.Chat وقد يتسبب في حدوث مشاكل مع تراخيص السوق والمؤسسة!", "True": "صحيح", "Try_now": "المحاولة الآن", "Try_searching_in_the_marketplace_instead": "محاولة البحث في السوق بدلاً من ذلك", @@ -4855,8 +4851,6 @@ "onboarding.page.requestTrial.subtitle": "جرب أفضل خطة إصدار Enterprise لمدة 30 يومًا مجانًا", "onboarding.page.magicLinkEmail.title": "أرسلنا لك رابط تسجيل الدخول عبر البريد الإلكتروني", "onboarding.page.magicLinkEmail.subtitle": "انقر فوق الرابط الموجود في البريد الإلكتروني الذي أرسلناه لك للتو لتسجيل الدخول إلى مساحة العمل الخاصة بك. <1>ستنتهي صلاحية الرابط خلال 30 دقيقة.", - "onboarding.page.organizationInfoPage.title": "بعض التفاصيل الإضافية...", - "onboarding.page.organizationInfoPage.subtitle": "ستساعدنا هذه على تخصيص مساحة العمل الخاصة بك.", "onboarding.form.adminInfoForm.title": "معلومات المسؤول", "onboarding.form.adminInfoForm.subtitle": "نحتاج إلى هذا لإنشاء ملف شخصي مسؤول داخل مساحة العمل الخاصة بك", "onboarding.form.adminInfoForm.fields.fullName.label": "الاسم الكامل", @@ -4885,10 +4879,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "التكامل مع مقدمي الخدمات الخارجيين (WhatsApp وFacebook وTelegram وTwitter)", "onboarding.form.registeredServerForm.included.apps": "الوصول إلى تطبيقات السوق", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "البريد الإلكتروني لحساب السحابة", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "لتسجيل الخادم الخاص بك، نحتاج إلى توصيله بحسابك السحابي. إذا كان لديك حساب سابقًا، فسنقوم بربطه تلقائيًا. وإن لم يكن لديك، فسيتم إنشاء حساب جديد", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "يرجى إدخال بريدك الإلكتروني", "onboarding.form.registeredServerForm.keepInformed": "أبقني على اطلاع بالأخبار والأحداث", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "يعني التسجيل موافقتي على تلقي تحديثات المنتج والأمان ذات الصلة", "onboarding.form.standaloneServerForm.title": "تأكيد الخادم المستقل", "onboarding.form.standaloneServerForm.servicesUnavailable": "لن تكون بعض الخدمات متاحة أو ستتطلب إعدادًا يدويًا", "onboarding.form.standaloneServerForm.publishOwnApp": "لإرسال الإشعارات، تحتاج إلى تجميع تطبيقك الخاص ونشره على Google Play وApp Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json index b6e15bbf6f66..9f0ef2e27a74 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json @@ -4227,10 +4227,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Aquesta configuració evita que totes les instàncies enviïn els canvis d'estat dels usuaris als clients, mantenint tots els usuaris amb el seu estat de presència des de la primera càrrega!", "Troubleshoot_Disable_Sessions_Monitor": "Desactiva el monitor de sessions", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Aquesta configuració deté el processament de les sessions de visita de l'LiveChat causant que les estadístiques deixin de funcionar!", - "Troubleshoot_Disable_Statistics_Generator": "Desactivar el generador d'estadístiques", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Aquest ajust deté el processament de totes les estadístiques fent que la pàgina d'informació quedi desactualitzada fins que algú faci clic al botó d'actualització i pot causar que falti altra informació en el sistema!", - "Troubleshoot_Disable_Workspace_Sync": "Desactiva la sincronització de l'espai de treball", - "Troubleshoot_Disable_Workspace_Sync_Alert": "¡Este ajuste detiene la sincronización de este servidor con la nube de Rocket.Chat y puede causar problemas con el mercado y las licencias de las empresas!", "True": "Sí", "Try_now": "Prova-ho ara", "Tuesday": "dimarts", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json index ff081e3a5eeb..c2fbddd26d29 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json @@ -3561,10 +3561,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Toto nastavení zakáže všem instancím odesílat změny stavu uživatelů a ponechat si nastavení při prvním načtení", "Troubleshoot_Disable_Sessions_Monitor": "Zakázat monitor sessions", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Toto nastavení přestane zpracovávat uživatelské sessions a statistiky tak přestanou správně fungovat!", - "Troubleshoot_Disable_Statistics_Generator": "Zakázat generování statistik", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Toto nastavení přestane zpracovávat statistiky, takže stránka s informacemi zůstane neaktuální dokud někdo nevynutí aktualizaci. Způsobuje neaktuálnost dat napříč systémem!", - "Troubleshoot_Disable_Workspace_Sync": "Zakázat synchronizaci pracovního prostoru", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Toto nastavení zakáže synchronizaci s Rocket.chat cloud a může způsobit problémy s marketplace a enterprise licencemi!", "True": "Ano", "Try_now": "Zkusit nyní", "Tuesday": "Úterý", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json index 317faac59164..66e3bb1f035e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json @@ -3582,10 +3582,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Denne indstilling forhindrer alle instancer fra at sende statusændringerne for brugerne til deres klienter, hvilket gør, at alle brugere vil have deres status vedr. tilstedeværelse fra de blev loadet i starten!", "Troubleshoot_Disable_Sessions_Monitor": "Deaktivér sessions-monitor", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Denne indstilling stopper behandlingen af brugersessioner og får statistikkerne til at stoppe med at virke korrekt!", - "Troubleshoot_Disable_Statistics_Generator": "Deaktivér statistik-generator", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Denne indstilling stopper behandlingen af alle statistikker, hvilket gør at informationssiden forældes, indtil nogen klikker på opdateringsknappen og kan også forårsage andre manglende oplysninger rundt omkring i systemet!", - "Troubleshoot_Disable_Workspace_Sync": "Deaktivér synkronisering af Workspace", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Denne indstilling stopper synkroniseringen af denne server med Rocket.Chat's cloud og kan forårsage problemer med marketplace og enteprise-licenser!", "True": "Sandt", "Try_now": "Forsøg nu", "Tuesday": "tirsdag", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json index 47649dd17b2d..52054a3ebdad 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json @@ -4821,10 +4821,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Diese Einstellung sorgt dafür, dass keine Instanz mehr die Statusänderungen der Benutzer an ihre Clients sendet, sodass die Benutzer den Präsenzstatus behalten, den sie beim ersten Laden hatten!", "Troubleshoot_Disable_Sessions_Monitor": "Sitzungsmonitor deaktivieren", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Diese Einstellung stoppt die Verarbeitung von Benutzersitzungen, was dazu führt, dass die Statistiken nicht mehr ordnungsgemäß funktionieren!", - "Troubleshoot_Disable_Statistics_Generator": "Statistikgenerator deaktivieren", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Diese Einstellung stoppt die Verarbeitung der gesamten Statistik, sodass die Infoseite so lange veraltet ist, bis jemand die Aktualisierungschaltfläche anklickt. Außerdem kann es sein, dass andere Systeminformationen fehlen!", - "Troubleshoot_Disable_Workspace_Sync": "Arbeitsbereichsynchronisierung deaktivieren", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Diese Einstellung stoppt die Synchronisierung des Servers mit der Rocket.Chat-Cloud und kann Probleme mit Marktplatz- und Unternehmenslizenzen verursachen!", "True": "Ja", "Try_now": "Jetzt versuchen", "Try_searching_in_the_marketplace_instead": "Versuchen Sie stattdessen den Marktplatz zu durchsuchen", @@ -4953,7 +4949,7 @@ "User__username__unmuted_in_room__roomName__": "Stummschaltung von Benutzer {{username}} in Raum {{roomName}} aufgehoben", "User_added": "Benutzer hinzugefügt", "User_added_by": "Der Benutzer {{user_added}} wurde von {{user_by}} hinzugefügt", - "User_added_to": "__user_added_ hinzugefügt", + "User_added_to": "hinzugefügt {{user_added}}", "User_added_successfully": "Benutzer erfolgreich hinzugefügt", "User_and_group_mentions_only": "Nur Benutzer- und Gruppenerwähnungen", "User_cant_be_empty": "Benutzer darf nicht leer sein", @@ -5466,8 +5462,6 @@ "onboarding.page.requestTrial.subtitle": "Testen Sie unseren besten Enterprise Edition-Plan 30 Tage lang gratis", "onboarding.page.magicLinkEmail.title": "Wir haben Ihnen einen Anmeldelink gesendet", "onboarding.page.magicLinkEmail.subtitle": "Klicken Sie auf den Link, in der gerade an Sie versandten E-Mail, um sich bei Ihrem Arbeitsbereich anzumelden. <1>Der Link verfällt in 30 Minuten.", - "onboarding.page.organizationInfoPage.title": "Ein paar zusätzliche Details...", - "onboarding.page.organizationInfoPage.subtitle": "Diese helfen uns, Ihren Arbeitsbereich zu personalisieren.", "onboarding.form.adminInfoForm.title": "Admin-Info", "onboarding.form.adminInfoForm.subtitle": "Das ist erforderlich, um ein Admin-Profil in Ihrem Arbeitsbereich zu erstellen", "onboarding.form.adminInfoForm.fields.fullName.label": "Vollständiger Name", @@ -5496,10 +5490,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integration mit externen Anbietern (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Zugriff auf Marktplatz-Apps", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Cloud-Konto-E-Mail", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Um Ihren Server zu registrieren, müssen wir ihn mit Ihrem Cloud-Konto verbinden. Wenn Sie bereits eines haben, werden wir es automatisch verknüpfen. Andernfalls wird ein neues Konto erstellt", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Bitte geben Sie Ihre E-Mail-Adresse ein", "onboarding.form.registeredServerForm.keepInformed": "Informieren Sie mich über Neuigkeiten und Ereignisse", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Mit der Registrierung stimme ich zu, dass ich relevante Produkt- und Sicherheits-Updates erhalte", "onboarding.form.standaloneServerForm.title": "Stand-alone-Server-Bestätigung", "onboarding.form.standaloneServerForm.servicesUnavailable": "Einige der Services werden nicht verfügbar sein oder erfordern eine manuelle Einrichtung", "onboarding.form.standaloneServerForm.publishOwnApp": "Um Push-Benachrichtigungen zu senden, müssen Sie Ihre eigene App kompilieren und in Google Play und im App Store veröffentlichen", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 2b47c15e9f9e..0a39692523eb 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -6087,4 +6087,4 @@ "Theme_Appearence": "Theme Appearence", "Premium": "Premium", "Premium_capability": "Premium capability" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json index 50a7b8d63873..aac9f8bfa4db 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json @@ -4268,10 +4268,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Este ajuste evita que todas las instancias envíen los cambios de estado de los usuarios a sus clientes, lo que mantiene todos los usuarios con su estado de presencia desde la primera carga.", "Troubleshoot_Disable_Sessions_Monitor": "Deshabilitar supervisor de sesiones", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Este ajuste detiene el procesamiento de las sesiones de visita de Omnichannel, lo que provoca que las estadísticas dejen de funcionar correctamente.", - "Troubleshoot_Disable_Statistics_Generator": "Deshabilitar generador de estadísticas", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Este ajuste detiene el procesamiento de todas las estadísticas, lo que provoca que la página de información quede desactualizada hasta que alguien haga clic en el botón para actualizar. Además, puede causar que falte otra información en el sistema.", - "Troubleshoot_Disable_Workspace_Sync": "Deshabilitar sincronización de espacio de trabajo", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Este ajuste detiene la sincronización de este servidor con la nube de Rocket.Chat y puede problemas con las licencias de empresas y Marketplace.", "True": "Verdadero", "Try_now": "Intentar ahora", "Try_searching_in_the_marketplace_instead": "Prueba a buscar en Marketplace en su lugar", @@ -4805,8 +4801,6 @@ "onboarding.page.requestTrial.subtitle": "Prueba nuestro mejor plan Enterprise Edition gratis durante 30 días", "onboarding.page.magicLinkEmail.title": "Te hemos enviado un enlace de inicio de sesión por correo electrónico", "onboarding.page.magicLinkEmail.subtitle": "Haz clic en el enlace del mensaje que acabamos de enviarte para iniciar sesión en tu espacio de trabajo. <1>El enlace caducará en 30 minutos.", - "onboarding.page.organizationInfoPage.title": "Unos detalles más...", - "onboarding.page.organizationInfoPage.subtitle": "Esto nos ayudará a personalizar tu espacio de trabajo.", "onboarding.form.adminInfoForm.title": "Información de administrador", "onboarding.form.adminInfoForm.subtitle": "Necesitamos esto para crear un perfil de administrador en tu espacio de trabajo", "onboarding.form.adminInfoForm.fields.fullName.label": "Nombre completo", @@ -4835,10 +4829,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integración con proveedores externos (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Acceso al Marketplace de aplicaciones", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Cuenta de correo electrónico en la nube", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Para registrar el servidor, necesitamos conectarlo a tu cuenta en la nube. Si ya tienes una, la vincularemos automáticamente. De lo contrario, se creará una cuenta nueva", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Introduce tu correo electrónico", "onboarding.form.registeredServerForm.keepInformed": "Recibir información sobre noticias y eventos", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Al registrarme, acepto recibir actualizaciones sobre seguridad y productos relevantes", "onboarding.form.standaloneServerForm.title": "Confirmación de servidor independiente", "onboarding.form.standaloneServerForm.servicesUnavailable": "Algunos servicios no estarán disponibles o requerirán configuración manual", "onboarding.form.standaloneServerForm.publishOwnApp": "Para enviarte notificaciones push, debes compilar y publicar tu propia aplicación en Google Play y App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json index 34c7ca9fdda4..79a27d83cbe6 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json @@ -4917,10 +4917,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Tämä asetus estää kaikkia instansseja lähettämästä käyttäjien tilamuutoksia asiakkailleen, jolloin kaikki käyttäjät pysyvät läsnäolotilassaan ensimmäisestä latauksesta lähtien!", "Troubleshoot_Disable_Sessions_Monitor": "Istuntojen valvonnan poistaminen käytöstä", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Tämä asetus pysäyttää käyttäjäistuntojen käsittelyn, jolloin tilastot eivät enää toimi oikein!", - "Troubleshoot_Disable_Statistics_Generator": "Poista tilastogeneraattori käytöstä", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Tämä asetus pysäyttää kaikkien tilastojen käsittelyn, jolloin infosivu on vanhentunut, kunnes joku klikkaa päivityspainiketta, ja se voi aiheuttaa muita puuttuvia tietoja järjestelmästä!", - "Troubleshoot_Disable_Workspace_Sync": "Työtilan synkronoinnin poistaminen käytöstä", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Tämä asetus estää tämän palvelimen synkronoinnin Rocket.Chatin pilven kanssa ja saattaa aiheuttaa ongelmia markkinapaikan ja enteprise-lisenssien kanssa!", "True": "Tosi", "Try_now": "Kokeile nyt", "Try_searching_in_the_marketplace_instead": "Kokeile sen sijaan etsiä Kauppapaikalta", @@ -5603,8 +5599,6 @@ "onboarding.page.requestTrial.subtitle": "Kokeile parasta yritysversion sopimustamme 30 päivää maksutta", "onboarding.page.magicLinkEmail.title": "Lähetimme sinulle kirjautumislinkin sähköpostitse", "onboarding.page.magicLinkEmail.subtitle": "Klikkaa juuri lähettämässämme sähköpostiviestissä olevaa linkkiä kirjautuaksesi työtilaasi. <1>Linkki päättyy 30 minuutin kuluttua.", - "onboarding.page.organizationInfoPage.title": "Muutama yksityiskohta vielä...", - "onboarding.page.organizationInfoPage.subtitle": "Nämä auttavat meitä muokkaamaan työtilasi yksilölliseksi.", "onboarding.form.adminInfoForm.title": "Admin Info", "onboarding.form.adminInfoForm.subtitle": "Tarvitsemme tätä luodaksemme ylläpitäjäprofiilin työtilaasi", "onboarding.form.adminInfoForm.fields.fullName.label": "Koko nimi", @@ -5633,12 +5627,10 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integrointi ulkoisten palveluntarjoajien kanssa (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Pääsy kauppapaikan sovelluksiin", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Cloud-tilin sähköposti", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Rekisteröidäksemme palvelimesi meidän on yhdistettävä se pilvitiliisi. Jos sinulla on jo sellainen - yhdistämme sen automaattisesti. Muussa tapauksessa luodaan uusi tili", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Kirjoita sähköpostiosoitteesi", "onboarding.form.registeredServerForm.keepInformed": "Pidä minut ajan tasalla uutisista ja tapahtumista", "onboarding.form.registeredServerForm.registerLater": "Rekisteröidy myöhemmin", "onboarding.form.registeredServerForm.notConnectedToInternet": "Palvelin ei ole yhteydessä internetiin, joten työtila on rekisteröitävä offline-tilassa.", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Rekisteröitymällä suostun vastaanottamaan asiaankuuluvia tuote- ja tietoturvapäivityksiä", "onboarding.form.standaloneServerForm.title": "Itsenäisen palvelimen vahvistus", "onboarding.form.standaloneServerForm.servicesUnavailable": "Jotkin palvelut eivät ole käytettävissä tai vaativat manuaalista asennusta", "onboarding.form.standaloneServerForm.publishOwnApp": "Jotta voit lähettää push-ilmoituksia, sinun on koottava ja julkaistava oma sovelluksesi Google Play- ja App Store -sovelluksissa", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json index 08641b831f94..f460fc0b61de 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json @@ -4300,10 +4300,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Ce paramètre empêche toutes les instances d'envoyer les changements de statut des utilisateurs à leurs clients ; le statut de présence du premier chargement est donc conservé !", "Troubleshoot_Disable_Sessions_Monitor": "Désactiver le moniteur de sessions", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Ce paramètre arrête le traitement des sessions utilisateur, ce qui empêche les statistiques de fonctionner correctement !", - "Troubleshoot_Disable_Statistics_Generator": "Désactiver le générateur de statistiques", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Ce paramètre arrête le traitement de toutes les statistiques, ce qui rend la page d'informations obsolète jusqu'à ce que quelqu'un clique sur le bouton d'actualisation ; d'autres informations peuvent être manquantes dans le système !", - "Troubleshoot_Disable_Workspace_Sync": "Désactiver la synchronisation de l'espace de travail", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Ce paramètre arrête la synchronisation de ce serveur avec le cloud Rocket.Chat et peut entraîner des problèmes avec les licences marketplace et entreprise !", "True": "Vrai", "Try_now": "Essayer maintenant", "Try_searching_in_the_marketplace_instead": "Essayez plutôt de chercher sur le marketplace", @@ -4855,8 +4851,6 @@ "onboarding.page.requestTrial.subtitle": "Essayez notre meilleur forfait Enterprise Edition gratuitement pendant 30 jours", "onboarding.page.magicLinkEmail.title": "Nous vous avons envoyé un lien de connexion par e-mail", "onboarding.page.magicLinkEmail.subtitle": "Cliquez sur le lien dans l'e-mail que nous venons de vous envoyer pour vous connecter à votre espace de travail. <1>Le lien expirera dans 30 minutes.", - "onboarding.page.organizationInfoPage.title": "Quelques détails supplémentaires...", - "onboarding.page.organizationInfoPage.subtitle": "Ceux-ci nous aideront à personnaliser votre espace de travail.", "onboarding.form.adminInfoForm.title": "Infos sur l'administrateur", "onboarding.form.adminInfoForm.subtitle": "Nous en avons besoin pour créer un profil d'administrateur dans votre espace de travail", "onboarding.form.adminInfoForm.fields.fullName.label": "Nom complet", @@ -4885,10 +4879,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Intégration avec des fournisseurs externes (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Accès aux applications du marketplace", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "E-mail du compte cloud", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Pour enregistrer votre serveur, nous devons le connecter à votre compte cloud. Si vous en avez déjà un, nous l'associerons automatiquement. Sinon, un nouveau compte sera créé", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Entrez votre adresse e-mail", "onboarding.form.registeredServerForm.keepInformed": "Me tenir informé des actualités et des événements", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "En m'inscrivant, j'accepte de recevoir des mises à jour pertinentes concernant les produits et la sécurité", "onboarding.form.standaloneServerForm.title": "Confirmation du serveur autonome", "onboarding.form.standaloneServerForm.servicesUnavailable": "Certains services ne seront pas disponibles ou nécessiteront une configuration manuelle", "onboarding.form.standaloneServerForm.publishOwnApp": "Pour envoyer des notifications push, vous devez compiler et publier votre propre application sur Google Play et App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/gl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/gl.i18n.json index 5272cd9e1d3f..9ee622195a6a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/gl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/gl.i18n.json @@ -352,8 +352,6 @@ "onboarding.page.requestTrial.subtitle": "Proba o noso mellor plana de empresas durante 30 días de balde", "onboarding.page.magicLinkEmail.title": "Enviámosche por correo electrónico un link de inicio de sesión", "onboarding.page.magicLinkEmail.subtitle": "Fai clic na ligazón do correo electrónico que che acabamos de enviar para iniciar sesión no teu espazo de traballo. <1>A ligazón caducará en 30 minutos.", - "onboarding.page.organizationInfoPage.title": "Algúns detalles máis...", - "onboarding.page.organizationInfoPage.subtitle": "Estes axudaranos a personalizar o teu espazo de traballo.", "onboarding.form.adminInfoForm.title": "Información administrativa", "onboarding.form.adminInfoForm.subtitle": "Necesitamos isto para crear un perfil de administrador dentro do teu espazo de traballo", "onboarding.form.adminInfoForm.fields.fullName.label": "Nome completo", @@ -382,10 +380,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integración con provedores externos (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Acceso a aplicacións do mercado", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Correo electrónico da conta na nube", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Para rexistrar o teu servidor, necesitamos conectalo á túa conta na nube. Se xa tes un, vincularémolo automaticamente. En caso contrario, crearase unha nova conta", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Introduce o teu correo electrónico", "onboarding.form.registeredServerForm.keepInformed": "Mantéñame informado sobre novidades e eventos", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Ao rexistrarme, acepto recibir actualizacións de produtos e seguridade relevantes", "onboarding.form.standaloneServerForm.title": "Confirmación do servidor autónomo", "onboarding.form.standaloneServerForm.servicesUnavailable": "Algúns dos servizos non estarán dispoñibles ou requirirán unha configuración manual", "onboarding.form.standaloneServerForm.publishOwnApp": "Para enviar notificacións push, debes compilar e publicar a túa propia aplicación en Google Play e App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json index e54f69c81052..1177a356dc71 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json @@ -4736,10 +4736,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Ez a beállítás megakadályozza az összes példányt abban, hogy elküldjék a felhasználók állapotváltozásait az ügyfeleiknek, megtartva az összes felhasználót az első betöltésből származó jelenléti állapotával!", "Troubleshoot_Disable_Sessions_Monitor": "Munkamenetek megfigyelőjének letiltása", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Ez a beállítás leállítja a felhasználók munkameneteinek feldolgozását, ami a statisztikák megfelelő működésének megszűnését okozza!", - "Troubleshoot_Disable_Statistics_Generator": "Statisztika-előállító letiltása", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Ez a beállítás leállítja az összes statisztika feldolgozását, ami az információs oldalt elavulttá teszi, amíg valaki nem kattint a frissítés gombra, valamint más információk hiányát is okozhatja a rendszerben!", - "Troubleshoot_Disable_Workspace_Sync": "Munkaterület szinkronizálásának letiltása", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Ez a beállítás leállítja ennek a kiszolgálónak a Rocket.Chat felhőjével való szinkronizálását, és problémákat okozhat a piactérrel és a vállalati licencekkel!", "True": "Igaz", "Try_now": "Próbálja most", "Try_searching_in_the_marketplace_instead": "Próbáljon inkább a piactéren keresni", @@ -5393,8 +5389,6 @@ "onboarding.page.requestTrial.subtitle": "Próbálja ki a legjobb vállalati kiadású előfizetéses csomagunkat 30 napig ingyen", "onboarding.page.magicLinkEmail.title": "Elküldünk Önnek egy bejelentkezési hivatkozást e-mailben", "onboarding.page.magicLinkEmail.subtitle": "Kattintson a most elküldött levélben lévő hivatkozásra, hogy bejelentkezhessen a munkaterületére. <1>A hivatkozás 30 percen belül lejár.", - "onboarding.page.organizationInfoPage.title": "Néhány további részlet…", - "onboarding.page.organizationInfoPage.subtitle": "Ezek segítenek nekünk személyre szabni a munkaterületét.", "onboarding.form.adminInfoForm.title": "Adminisztrátor-információk", "onboarding.form.adminInfoForm.subtitle": "Erre azért van szükségünk, hogy létrehozzunk egy adminisztrátori profilt a munkaterületén belül", "onboarding.form.adminInfoForm.fields.fullName.label": "Teljes név", @@ -5423,10 +5417,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integráció külső szolgáltatókkal (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Hozzáférés a piactér alkalmazásaihoz", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Felhős fiók e-mail-címe", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "A kiszolgálója regisztrálásához csatlakoztatnunk kell azt a felhős fiókjához. Ha már rendelkezik ilyennel, akkor automatikusan összekapcsoljuk. Ellenkező esetben új fiók kerül létrehozásra.", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Adja meg az e-mail-címét", "onboarding.form.registeredServerForm.keepInformed": "Tájékoztassanak a hírekről és az eseményekről", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "A regisztrációval beleegyezem, hogy megkapom a vonatkozó termék- és biztonsági frissítéseket", "onboarding.form.standaloneServerForm.title": "Egyedülálló kiszolgáló megerősítése", "onboarding.form.standaloneServerForm.servicesUnavailable": "Néhány szolgáltatás nem lesz elérhető, vagy kézi beállítást igényel", "onboarding.form.standaloneServerForm.publishOwnApp": "A leküldéses értesítések küldéséhez saját alkalmazást kell összeállítania és közzétennie a Google Play és az App Store áruházakban", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json index 0bf0eb5063ae..31973b5da92a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json @@ -4259,10 +4259,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "この設定は、すべてのインスタンスがユーザーのステータス変更をクライアントに送信することを防ぎ、すべてのユーザーを最初のロードからのプレゼンスステータスで維持します!", "Troubleshoot_Disable_Sessions_Monitor": "セッションモニターを無効にする", "Troubleshoot_Disable_Sessions_Monitor_Alert": "この設定により、ユーザーセッションの処理が停止し、統計が正しく機能しなくなります!", - "Troubleshoot_Disable_Statistics_Generator": "統計ジェネレーターを無効にする", - "Troubleshoot_Disable_Statistics_Generator_Alert": "この設定では、更新ボタンがクリックされるまですべての統計処理が停止され、情報ページの情報が最新ではなくなり、システムに関するその他の情報が失われる可能性があります。", - "Troubleshoot_Disable_Workspace_Sync": "ワークスペース同期を無効にする", - "Troubleshoot_Disable_Workspace_Sync_Alert": "この設定により、このサーバーとRocket.Chatのクラウドとの同期が停止し、マーケットプレイスとエンタープライズライセンスで問題が発生する可能性があります!", "True": "はい", "Try_now": "今すぐ再試行", "Try_searching_in_the_marketplace_instead": "代わりにマーケットプレイスを検索してみてください", @@ -4805,8 +4801,6 @@ "onboarding.page.requestTrial.subtitle": "30日間の最上位のEnterprise Editionプランを無料でお試しください", "onboarding.page.magicLinkEmail.title": "ログインリンクをメールで送信しました", "onboarding.page.magicLinkEmail.subtitle": "送信されたメールのリンクをクリックし、ワークスペースにサインインしてください。 <1>リンクの有効期間は30分です。", - "onboarding.page.organizationInfoPage.title": "その他の詳細...", - "onboarding.page.organizationInfoPage.subtitle": "これにより、ワークスペースをパーソナライズできます。", "onboarding.form.adminInfoForm.title": "管理者情報", "onboarding.form.adminInfoForm.subtitle": "これはワークスペース内に管理プロファイルを作成するために必要です", "onboarding.form.adminInfoForm.fields.fullName.label": "氏名", @@ -4835,10 +4829,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "外部プロバイダー(WhatsApp、Facebook、Telegram、Twitter)との統合", "onboarding.form.registeredServerForm.included.apps": "マーケットプレイスアプリにアクセス", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "クラウドアカウントメール", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "サーバーを登録するには、サーバーをクラウドアカウントに接続する必要があります。アカウントをすでにお持ちの場合は、自動的にリンクします。それ以外の場合は、新しいアカウントが作成されます", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "メールアドレスを入力してください", "onboarding.form.registeredServerForm.keepInformed": "ニュースとイベントの情報を受け取る", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "登録すると、関連する製品とセキュリティの更新を受け取ることに同意したものとみなされます", "onboarding.form.standaloneServerForm.title": "スタンドアロンサーバーの確認", "onboarding.form.standaloneServerForm.servicesUnavailable": "一部のサービスは利用できないか、手動で設定する必要があります", "onboarding.form.standaloneServerForm.publishOwnApp": "プッシュ通知を送信するには、独自のアプリをコンパイルしてGoogle PlayとApp Storeに公開する必要があります", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json index 15514a726e5b..6dd72917fe9d 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json @@ -3303,10 +3303,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "ეს პარამეტრი ყვენა ინსტანციისთვის თიშავს მომხმარებლის სტატუსის გადაგზავნას კლიენტებისთვის და ტოვებს ყველა მომხმარებელს იმ სტატუსით რომლითაც იყო პირველი ჩატვირთვისას.", "Troubleshoot_Disable_Sessions_Monitor": "გამორთეთ სესიების მონიტორი", "Troubleshoot_Disable_Sessions_Monitor_Alert": "ეს პარამეტრი თიშავს მომხმარებლის სესიების დამუშავებას და იწვევს სტატისტიკის არასწორ მუშაობას", - "Troubleshoot_Disable_Statistics_Generator": "გამორთეთ სტატისტიკის გენერატორი", - "Troubleshoot_Disable_Statistics_Generator_Alert": "ეს პარამეტრი სტატისტიკის დამუშავებას თიშავს სრულად და გვერდი ხდება ვადაგასული ვიდრე ვინმე განახლების ღილაკს არ დააჭერს, ამან შეიძლება გამოიწვიოს ზოგი ინფორმაციის დაკარგვა", - "Troubleshoot_Disable_Workspace_Sync": "გამორთეთ სამუშაო ადგილის სინქრონიზაცია", - "Troubleshoot_Disable_Workspace_Sync_Alert": "ეს პარამეტრი თიშავს ამ სერვერის Rocket.Chat's clou-თან სინქრონიზაციას და შეიძლება გამოიწვიოს პრობლემები მარკეტში და საწარმო ლიცენზიებში!", "True": "მართალია", "Try_now": "სცადე ახლა", "Tuesday": "სამშაბათი", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json index 34f7608c9bed..2574c0b288b8 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json @@ -3624,10 +3624,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "이 설정은 모든 인스턴스가 사용자의 상태 변경 사항을 클라이언트로 보내지 않으며, 설정 시, 모든 사용자의 상태가 처음 로딩상태로 유지됩니다.!", "Troubleshoot_Disable_Sessions_Monitor": "세션 모니터링 사용중지", "Troubleshoot_Disable_Sessions_Monitor_Alert": "이 설정은 사용자 세션 처리를 중단하는 것입니다. 설정 시 통계가 올바르게 작동하지 않을 수 있습니다. ", - "Troubleshoot_Disable_Statistics_Generator": "통계 생성 사용중지", - "Troubleshoot_Disable_Statistics_Generator_Alert": "이 설정은 누군가가 새로 고침 버튼을 클릭 할 때까지 이전 정보 페이지를 생성하는 모든 통계 처리를 중지하는 것입니다. 설정 시 시스템 주변에 다른 정보가 누락 될 수 있습니다!", - "Troubleshoot_Disable_Workspace_Sync": " Workspace 동기화 사용중지", - "Troubleshoot_Disable_Workspace_Sync_Alert": "이 설정은 서버와 Rocket.Chat의 클라우드 동기화를 중지하는 것입니다. 설정 시 Marketplace 및 기업 라이선스에 문제가 발생할 수 있습니다. ", "True": "설정됨", "Try_now": "지금 시도", "Tuesday": "화요일", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json index f611b39aebd4..eff6811050c1 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json @@ -4290,10 +4290,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Deze instelling voorkomt dat alle instanties de statuswijzingen van de gebruikers naar hun clients sturen, waarbij alle gebruikers hun aanwezigheidsstatus behouden van de eerste lading!", "Troubleshoot_Disable_Sessions_Monitor": "Schakel sessies monitor uit", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Deze instelling stopt de verwerking van gebruikerssessies waardoor de statistieken niet meer correct werken!", - "Troubleshoot_Disable_Statistics_Generator": "Schakel statistieken generator uit", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Deze instelling stopt de verwerking van alle statistieken waardoor de info-pagina verouderd raakt totdat iemand op de Vernieuwen knop klikt, en kan ontbrekende informatie in het systeem verzoorzaken!", - "Troubleshoot_Disable_Workspace_Sync": "Schakel Workspace Sync uit", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Deze instelling stopt de synchronisatie van deze server met Rocket.Chat's cloud en kan problemen veroorzaken met marktplaats- en bedrijfslicenties!", "True": "Waar", "Try_now": "Probeer nu", "Try_searching_in_the_marketplace_instead": "Probeer in plaats daarvan in de Marketplace te zoeken", @@ -4843,8 +4839,6 @@ "onboarding.page.requestTrial.subtitle": "Probeer gratis onze beste Enterprise Edition-abonnement gedurende 30 dagen", "onboarding.page.magicLinkEmail.title": "We hebben je een inloglink gemaild.", "onboarding.page.magicLinkEmail.subtitle": "Klik op de link in de e-mail die we u zojuist hebben gestuurd om u aan te melden bij uw werkruimte. <1>De link verloopt over 30 minuten.", - "onboarding.page.organizationInfoPage.title": "Nog een paar details...", - "onboarding.page.organizationInfoPage.subtitle": "Deze zullen ons helpen om uw werkruimte te personaliseren.", "onboarding.form.adminInfoForm.title": "Admin info", "onboarding.form.adminInfoForm.subtitle": "We hebben dit nodig om een beheerdersprofiel in uw werkruimte te maken", "onboarding.form.adminInfoForm.fields.fullName.label": "Volledige naam", @@ -4873,10 +4867,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integratie met externe providers (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Toegang tot Marketplace-apps", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "E-mailadres van cloudaccount", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Om uw server te registreren, moeten we deze verbinden met uw cloudaccount. Als u er al een heeft, zullen we deze automatisch koppelen. Anders wordt er een nieuwe account aangemaakt", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Voer uw e-mailadres in", "onboarding.form.registeredServerForm.keepInformed": "Hou me op de hoogte van nieuws en evenementen", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Door te registreren ga ik akkoord met het ontvangen van relevante product- en beveiligingsupdates", "onboarding.form.standaloneServerForm.title": "Standalone serverbevestiging", "onboarding.form.standaloneServerForm.servicesUnavailable": "Sommige diensten zullen niet beschikbaar zijn of vereisen handmatige configuratie", "onboarding.form.standaloneServerForm.publishOwnApp": "Om pushmeldingen te verzenden, moet u uw eigen app compileren en publiceren in Google Play en App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json index 0c4b85e588e0..80ab48d383c0 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json @@ -4667,10 +4667,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "To ustawienie zapobiega wysyłaniu przez wszystkie instancje zmian statusu użytkowników do ich klientów, utrzymując status wszystkich użytkowników taki jak przy pierwszym załadowaniu!", "Troubleshoot_Disable_Sessions_Monitor": "Wyłącz monitor sesji", "Troubleshoot_Disable_Sessions_Monitor_Alert": "To ustawienie zatrzymuje przetwarzanie sesji użytkowników, co spowoduje niepoprawne działanie statystyk!", - "Troubleshoot_Disable_Statistics_Generator": "Wyłącz generator statystyk", - "Troubleshoot_Disable_Statistics_Generator_Alert": "To ustawienie zatrzymuje przetwarzanie wszystkich statystyk powodując, że strona informacyjna stanie się nieaktualna dopóki nie zostanie naciśnięty przycisk odświeżania i może wywołać utratę innych informacji w całym systemie!", - "Troubleshoot_Disable_Workspace_Sync": "Wyłączenie Workspace Sync", - "Troubleshoot_Disable_Workspace_Sync_Alert": "To ustawienie zatrzymuje synchronizację tego serwera z chmurą Rocket.Chat co może wywołać problemy z marketplace i licencjami korporacyjnymi!", "True": "Tak", "Try_now": "Spróbuj teraz", "Try_searching_in_the_marketplace_instead": "Zamiast tego spróbuj poszukać w Marketplace", @@ -5304,8 +5300,6 @@ "onboarding.page.requestTrial.subtitle": "Wypróbuj nasz najlepszy plan Enterprise Edition przez 30 dni za darmo", "onboarding.page.magicLinkEmail.title": "Wysłaliśmy Ci link do logowania", "onboarding.page.magicLinkEmail.subtitle": "Kliknij link w wiadomości e-mail, którą właśnie do Ciebie wysłaliśmy, aby zalogować się do swojego obszaru roboczego. <1>Link wygaśnie za 30 minut.", - "onboarding.page.organizationInfoPage.title": "Jeszcze kilka szczegółów...", - "onboarding.page.organizationInfoPage.subtitle": "Pomogą nam one spersonalizować Twoje miejsce pracy.", "onboarding.form.adminInfoForm.title": "Admin Info", "onboarding.form.adminInfoForm.subtitle": "Potrzebujemy tego, aby utworzyć profil administratora w twoim obszarze roboczym", "onboarding.form.adminInfoForm.fields.fullName.label": "Pełna nazwa", @@ -5334,10 +5328,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integracja z zewnętrznymi dostawcami (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Dostęp do aplikacji w Marketplace", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "E-mail konta w chmurze", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Aby zarejestrować Twój serwer, musimy połączyć go z Twoim kontem w chmurze. Jeśli już je posiadasz - połączymy je automatycznie. W przeciwnym razie, zostanie utworzone nowe konto", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Proszę wpisać swój adres e-mail", "onboarding.form.registeredServerForm.keepInformed": "Informuj mnie o nowościach i wydarzeniach", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Rejestrując się, wyrażam zgodę na otrzymywanie odpowiednich aktualizacji produktów i zabezpieczeń", "onboarding.form.standaloneServerForm.title": "Potwierdzenie serwera standalone", "onboarding.form.standaloneServerForm.servicesUnavailable": "Niektóre z usług będą niedostępne lub będą wymagały ręcznej konfiguracji", "onboarding.form.standaloneServerForm.publishOwnApp": "W celu wysyłania powiadomień push należy skompilować i opublikować własną aplikację w Google Play i App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index a571de313862..7dd1b47f335e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -4362,10 +4362,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Esta configuração impede todas as instâncias de enviar as alterações de status dos usuários aos seus clientes, mantendo o status de presença do primeiro carregamento!", "Troubleshoot_Disable_Sessions_Monitor": "Desativar monitor de sessões", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Esta configuração interrompe o processamento das sessões do usuário, fazendo com que as estatísticas parem de funcionar corretamente!", - "Troubleshoot_Disable_Statistics_Generator": "Desativar gerador de estatísticas", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Esta configuração interrompe o processamento de todas as estatísticas, tornando a informação da página desatualizada até que alguém clique no botão Atualizar, e poderá causar perda de outras informações em todo o sistema!", - "Troubleshoot_Disable_Workspace_Sync": "Desativa a sincronização do espaço de trabalho", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Esta configuração interrompe a sincronização deste servidor com a nuvem do Rocket.Chat e pode causar problemas com licenças de marketplace e enteprise!", "True": "Verdadeiro", "Try_now": "Tentar agora", "Try_searching_in_the_marketplace_instead": "Tente pesquisar no marketplace", @@ -4929,8 +4925,6 @@ "onboarding.page.requestTrial.subtitle": "Experimento nosso melhor plano Enterprise Edition grátis por 30 dias", "onboarding.page.magicLinkEmail.title": "Nós enviamos um link de login por e-mail", "onboarding.page.magicLinkEmail.subtitle": "Clique no link no e-mail que enviamos para iniciar sessão em seu espaço de trabalho. <1>O link vai expirar em 30 minutos.", - "onboarding.page.organizationInfoPage.title": "Mais alguns detalhes...", - "onboarding.page.organizationInfoPage.subtitle": "Isso nos ajudará a personalizar seu espaço de trabalho.", "onboarding.form.adminInfoForm.title": "Informação administrativa", "onboarding.form.adminInfoForm.subtitle": "Precisamos disso para criar um perfil de administração dentro do seu espaço de trabalho.", "onboarding.form.adminInfoForm.fields.fullName.label": "Nome completo", @@ -4959,10 +4953,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integração com provedores externos (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Acesso a aplicativos de Marketplace", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "E-mail da conta da nuvem", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Para registrar seu servidor, precisamos conectar à sua conta da nuvem. Se você já tem uma - nós conectaremos a ela automaticamente. Caso contrário, uma nova conta será criada", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Insira seu e-mail", "onboarding.form.registeredServerForm.keepInformed": "Mantenha-me informado sobre notícias e eventos", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Ao registrar, eu concordo em receber atualizações relevantes do produto e de segurança", "onboarding.form.standaloneServerForm.title": "Confirmação de servidor standalone", "onboarding.form.standaloneServerForm.servicesUnavailable": "Alguns dos serviços estarão indisponíveis ou precisarão de configuração manual", "onboarding.form.standaloneServerForm.publishOwnApp": "Para enviar notificações de push, você precisará compilar e publicar seu próprio aplicativo no Google Play e App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json index 8f71b6f49ab2..48557a3a146e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json @@ -4463,10 +4463,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Эта настройка не позволяет всем инстансам отправлять изменения статуса пользователей своим клиентам, сохраняя всех пользователей со статусом присутствия с первой загрузки!", "Troubleshoot_Disable_Sessions_Monitor": "Отключить монитор сессий", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Эта настройка останавливает обработку пользовательских сессий, в результате чего статистика перестает работать корректно!", - "Troubleshoot_Disable_Statistics_Generator": "Отключить генератор статистики", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Эта настройка останавливает обработку всей статистики, делая информационную страницу устаревшей до тех пор, пока кто-нибудь не нажмет кнопку обновления, это может привести к появлению другой недостающей информации в системе!", - "Troubleshoot_Disable_Workspace_Sync": "Отключить синхронизацию рабочего пространства", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Эта настройка останавливает синхронизацию данного сервера с Rocket.Chat Cloud и может привести к проблемам с корпоративами лицензиями и в магазине приложений!", "True": "Да", "Try_now": "Попробуйте сейчас", "Try_searching_in_the_marketplace_instead": "Попробуйте выполнить поиск в магазине", @@ -5041,8 +5037,6 @@ "onboarding.page.requestTrial.subtitle": "Воспользуйтесь нашим лучшим тарифным планом Enterprise Edition в течение 30 дней бесплатно", "onboarding.page.magicLinkEmail.title": "Мы отправили вам ссылку для входа в систему в электронном письме", "onboarding.page.magicLinkEmail.subtitle": "Нажмите на ссылку в электронном письме, чтобы войти в свое рабочее пространство. <1>Срок действия ссылки истечет через 30 минут.", - "onboarding.page.organizationInfoPage.title": "Дополнительные сведения...", - "onboarding.page.organizationInfoPage.subtitle": "Это поможет нам персонализировать ваше рабочее пространство.", "onboarding.form.adminInfoForm.title": "Информация об администраторе", "onboarding.form.adminInfoForm.subtitle": "Это необходимо для создания профиля администратора в вашем рабочем пространстве", "onboarding.form.adminInfoForm.fields.fullName.label": "Полное имя", @@ -5071,10 +5065,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Интеграция с внешними поставщиками (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Доступ к приложениям магазина", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Адрес электронной почты учетной записи в облаке", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Чтобы зарегистрировать сервер, необходимо подключить его к учетной записи облака. Если у вас уже есть такая учетная запись, мы свяжем ее автоматически. В противном случае будет создана новая учетная запись", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Введите адрес электронной почты", "onboarding.form.registeredServerForm.keepInformed": "Сообщайте мне новости и информацию о событиях", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Регистрируясь, я соглашаюсь получать соответствующие обновления продуктов и системы безопасности", "onboarding.form.standaloneServerForm.title": "Подтверждение автономного сервера", "onboarding.form.standaloneServerForm.servicesUnavailable": "Некоторые сервисы будут недоступны или потребуется ручная настройка", "onboarding.form.standaloneServerForm.publishOwnApp": "Чтобы отправлять push-уведомления, необходимо создать и опубликовать собственное приложение в Google Play и App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json index bea75b85c976..5cf724dd49c1 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json @@ -4922,10 +4922,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Med den här inställningen förhindras alla instanser att skicka ändringar av användarnas status till deras klienter, vilket gör att alla användare behåller sin närvarostatus från den första inläsningen.", "Troubleshoot_Disable_Sessions_Monitor": "Inaktivera sessionsövervakning", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Med den här inställningen stoppas bearbetningen av användarsessioner, vilket gör att statistikfunktionen slutar fungera som den ska.", - "Troubleshoot_Disable_Statistics_Generator": "Inaktivera generering av statistik", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Med den här inställningen stoppas bearbetningen av all statistik. Det gör att informationssidan blir inaktuell tills någon klickar på uppdateringsknappen och kan leda till att annan information saknas i systemet.", - "Troubleshoot_Disable_Workspace_Sync": "Inaktivera synkronisering av arbetsyta", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Den här inställningen stoppar serverns synkronisering med Rocket.Chat-molnet och kan orsaka problem med Marketplace och Enterprise-licenser.", "True": "Sant", "Try_now": "Pröva nu", "Try_searching_in_the_marketplace_instead": "Pröva att söka i Marketplace istället", @@ -5608,8 +5604,6 @@ "onboarding.page.requestTrial.subtitle": "Prova på vårt bästa Enterprise Edition-abonnemang i 30 dagar utan kostnad", "onboarding.page.magicLinkEmail.title": "Vi har skickat en inloggningslänk via e-post ", "onboarding.page.magicLinkEmail.subtitle": "Logga in på arbetsytan genom att klicka på länken i e-postmeddelandet vi precis skickade till dig. <1>Länken upphör att gälla om 30 minuter.", - "onboarding.page.organizationInfoPage.title": "Några uppgifter till...", - "onboarding.page.organizationInfoPage.subtitle": "Vi behöver dem för att anpassa arbetsytan.", "onboarding.form.adminInfoForm.title": "Information om administratör", "onboarding.form.adminInfoForm.subtitle": "Vi behöver skapa en administratörsprofil i din arbetsyta", "onboarding.form.adminInfoForm.fields.fullName.label": "Fullständigt namn", @@ -5638,12 +5632,10 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integrering med externa leverantörer (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Åtkomst till appar i Marketplace", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "E-postadress för molnkonto", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "För att registrera servern behöver vi ansluta den till ditt molnkonto. Om du har ett kopplar vi det automatiskt. Annars skapas ett nytt konto", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Ange din e-postadress", "onboarding.form.registeredServerForm.keepInformed": "Håll mig informerad om nyheter och händelser", "onboarding.form.registeredServerForm.registerLater": "Registrera dig senare", "onboarding.form.registeredServerForm.notConnectedToInternet": "Servern är inte ansluten till internet, så du måste göra en offline-registrering för den här arbetsytan.", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Genom att registrera mig godkänner jag att ta emot relevanta produkt- och säkerhetsuppdateringar", "onboarding.form.standaloneServerForm.title": "Bekräftelse av fristående server", "onboarding.form.standaloneServerForm.servicesUnavailable": "Vissa av tjänsterna kommer att vara otillgängliga eller måste ställas in manuellt", "onboarding.form.standaloneServerForm.publishOwnApp": "Om du ska kunna skicka pushmeddelanden måste du kompilera och publicera din egen app på Google Play och App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json index 5ad5c0eb1d2b..24dedfcf33e7 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json @@ -4068,10 +4068,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "這個設定可防止所有實務將使用者的狀態更改發送到其客戶端,從而使所有使用者保持首次載入的狀態!", "Troubleshoot_Disable_Sessions_Monitor": "停用 Sessions 監視器", "Troubleshoot_Disable_Sessions_Monitor_Alert": "這個設定將停止處理使用者 sessions,將會導致統計資料無法正常工作!", - "Troubleshoot_Disable_Statistics_Generator": "停用統計資料產生器", - "Troubleshoot_Disable_Statistics_Generator_Alert": "這個設定將停止處理所有統計資料,將會使資料頁面過時,直到有人點擊“重整”按鈕,並可能導致系統周圍缺少其他資料!", - "Troubleshoot_Disable_Workspace_Sync": "停用工作區同步", - "Troubleshoot_Disable_Workspace_Sync_Alert": "這個設定將停止該伺服器與 Rocket.Chat 的雲端同步,並可能導致商店和企業授權出現問題!", "True": "是", "Try_now": "現在再試", "Tuesday": "星期二", @@ -4553,8 +4549,6 @@ "onboarding.page.requestTrial.subtitle": "試用我們最棒的企業版方案,30 天免費", "onboarding.page.magicLinkEmail.title": "我們已透過電子郵件傳送登入連結給您", "onboarding.page.magicLinkEmail.subtitle": "按一下我們剛傳送給您的電子郵件中的連結,即可登入您的工作空間。<1>該連結將在 30 分鐘後到期。", - "onboarding.page.organizationInfoPage.title": "更多詳細資料...", - "onboarding.page.organizationInfoPage.subtitle": "這些資料將可協助我們個人化您的工作空間。", "onboarding.form.adminInfoForm.title": "管理員資訊", "onboarding.form.adminInfoForm.subtitle": "我們需要此資訊以在您的工作空間內建立管理員個人資料", "onboarding.form.adminInfoForm.fields.fullName.label": "全名", @@ -4583,10 +4577,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "與外部提供者 (WhatsApp、Facebook、Telegram、Twitter) 整合", "onboarding.form.registeredServerForm.included.apps": "存取市集應用程式", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "雲端帳戶電子郵件", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "若要註冊您的伺服器,我們需要將伺服器連線至您的雲端帳戶。如果您已有雲端帳戶,我們將會自動為您連線。否則,將需要建立新的帳戶", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "請輸入您的電子郵件", "onboarding.form.registeredServerForm.keepInformed": "在有新聞與活動消息時通知我", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "註冊即代表我同意收到相關產品與安全性更新資訊", "onboarding.form.standaloneServerForm.title": "獨立伺服器確認", "onboarding.form.standaloneServerForm.servicesUnavailable": "部分服務將無法使用或是需要手動設定", "onboarding.form.standaloneServerForm.publishOwnApp": "若要傳送推播通知,您必須對您所擁有的應用程式進行編碼,並將應用程式發佈至 Google Play 和 App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json index df4642f4b0df..283944175541 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json @@ -3717,10 +3717,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "这个设置可以防止所有的实例将用户的状态变化发送给他们的客户端,使所有的用户保持他们第一次加载时的存在状态。", "Troubleshoot_Disable_Sessions_Monitor": "禁用会话监控", "Troubleshoot_Disable_Sessions_Monitor_Alert": "这个设置停止了对用户会话的处理,将导致统计工作无法正常进行!", - "Troubleshoot_Disable_Statistics_Generator": "禁用统计生成器", - "Troubleshoot_Disable_Statistics_Generator_Alert": "这个设置会停止处理所有的统计数据,使信息页面过时,直到有人点击刷新按钮,并可能导致系统中的其他信息缺失!", - "Troubleshoot_Disable_Workspace_Sync": "禁用工作区同步", - "Troubleshoot_Disable_Workspace_Sync_Alert": "该设置会停止同步该服务器到 Rocket.Chat 云端,并可能导致市场和企业许可证出现问题!", "True": "是", "Try_now": "立即尝试", "Tuesday": "星期二", From 82839a4a2f31eac62df415a07d3c180db8c077dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Wed, 11 Oct 2023 19:46:44 -0300 Subject: [PATCH 02/38] chore: Remove text decoration from room tag (#30606) Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com> --- .../room/body/RoomForeword/RoomForewordUsernameListItem.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameListItem.tsx b/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameListItem.tsx index 5ac168b91846..fe148aa79d78 100644 --- a/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameListItem.tsx +++ b/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameListItem.tsx @@ -12,11 +12,12 @@ type RoomForewordUsernameListItemProps = { useRealName: boolean; }; +// TODO: Improve `Tag` a11y to be used as a link const RoomForewordUsernameListItem: VFC = ({ username, href, useRealName }) => { const { data, isLoading, isError } = useUserInfoQuery({ username }); return ( - + } className='mention-link' data-username={username} large> {isLoading && } {!isLoading && isError && username} From e8eeb2a79df8cb06ea9419dfa011a1c96b87744d Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Thu, 12 Oct 2023 12:12:12 -0300 Subject: [PATCH 03/38] fix: Threads breaking after sending messages too fast (#30622) Co-authored-by: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> --- .changeset/cool-zoos-move.md | 5 +++++ .../Threads/hooks/useThreadMainMessageQuery.ts | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 .changeset/cool-zoos-move.md diff --git a/.changeset/cool-zoos-move.md b/.changeset/cool-zoos-move.md new file mode 100644 index 000000000000..dda6fbe2b02e --- /dev/null +++ b/.changeset/cool-zoos-move.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fixed threads breaking when sending messages too fast diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts index 8b3bef03f793..aca714549cf1 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts +++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts @@ -1,13 +1,12 @@ -import { isThreadMainMessage } from '@rocket.chat/core-typings'; import type { IMessage, IThreadMainMessage } from '@rocket.chat/core-typings'; import { useStream } from '@rocket.chat/ui-contexts'; import type { UseQueryResult } from '@tanstack/react-query'; import { useQueryClient, useQuery } from '@tanstack/react-query'; import { useCallback, useEffect, useRef } from 'react'; +import { withDebouncing } from '../../../../../../lib/utils/highOrderFunctions'; import type { FieldExpression, Query } from '../../../../../lib/minimongo'; import { createFilterFromQuery } from '../../../../../lib/minimongo'; -import { onClientMessageReceived } from '../../../../../lib/onClientMessageReceived'; import { useRoom } from '../../../contexts/RoomContext'; import { useGetMessageByID } from './useGetMessageByID'; @@ -87,19 +86,22 @@ export const useThreadMainMessageQuery = ( }, [tmid]); return useQuery(['rooms', room._id, 'threads', tmid, 'main-message'] as const, async ({ queryKey }) => { - const message = await getMessage(tmid); + const mainMessage = await getMessage(tmid); - const mainMessage = (await onClientMessageReceived(message)) || message; - - if (!mainMessage && !isThreadMainMessage(mainMessage)) { + if (!mainMessage) { throw new Error('Invalid main message'); } + const debouncedInvalidate = withDebouncing({ wait: 10000 })(() => { + queryClient.invalidateQueries(queryKey, { exact: true }); + }); + unsubscribeRef.current = unsubscribeRef.current || subscribeToMessage(mainMessage, { - onMutate: () => { - queryClient.invalidateQueries(queryKey, { exact: true }); + onMutate: (message) => { + queryClient.setQueryData(queryKey, () => message); + debouncedInvalidate(); }, onDelete: () => { onDelete?.(); From 389bac82751cb905a90584ff43c8e10e7cdae9ca Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 12 Oct 2023 17:46:11 -0300 Subject: [PATCH 04/38] test: More tests for groups kick (#30536) --- apps/meteor/tests/end-to-end/api/03-groups.js | 172 +++++++++++++++++- 1 file changed, 166 insertions(+), 6 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/03-groups.js b/apps/meteor/tests/end-to-end/api/03-groups.js index e875be80fd3b..3941df1366eb 100644 --- a/apps/meteor/tests/end-to-end/api/03-groups.js +++ b/apps/meteor/tests/end-to-end/api/03-groups.js @@ -481,21 +481,181 @@ describe('[Groups]', function () { }); describe('/groups.kick', () => { - it('should remove user from group', (done) => { - request + let testUserModerator; + let credsModerator; + let testUserOwner; + let credsOwner; + let testUserMember; + let groupTest; + + const inviteUser = async (userId) => { + await request + .post(api('groups.invite')) + .set(credsOwner) + .send({ + roomId: groupTest._id, + userId, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }; + + before(async () => { + // had to do them in serie because calling them with Promise.all was failing some times + testUserModerator = await createUser(); + testUserOwner = await createUser(); + testUserMember = await createUser(); + + credsModerator = await login(testUserModerator.username, password); + credsOwner = await login(testUserOwner.username, password); + + await request + .post(api('groups.create')) + .set(credsOwner) + .send({ + name: `kick-test-group-${Date.now()}`, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group._id'); + expect(res.body).to.have.nested.property('group.t', 'p'); + expect(res.body).to.have.nested.property('group.msgs', 0); + groupTest = res.body.group; + }); + + await inviteUser(testUserModerator._id); + + await request + .post(api('groups.addModerator')) + .set(credsOwner) + .send({ + roomId: groupTest._id, + userId: testUserModerator._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }); + + after(async () => { + await Promise.all([ + request + .post(api('groups.delete')) + .set(credsOwner) + .send({ + roomId: groupTest._id, + }) + .expect('Content-Type', 'application/json') + .expect(200), + // updatePermission('kick-user-from-any-p-room', []), + updatePermission('remove-user', ['admin', 'owner', 'moderator']), + deleteUser(testUserModerator), + deleteUser(testUserOwner), + deleteUser(testUserMember), + ]); + }); + + it("should return an error when user is not a member of the group and doesn't have permission", async () => { + await request + .post(api('groups.kick')) + .set(credentials) + .send({ + roomId: groupTest._id, + userId: testUserMember._id, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-room-not-found'); + }); + }); + + it('should allow a moderator to remove user from group', async () => { + await inviteUser(testUserMember._id); + + await request + .post(api('groups.kick')) + .set(credsModerator) + .send({ + roomId: groupTest._id, + userId: testUserMember._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }); + + it('should allow an owner to remove user from group', async () => { + await inviteUser(testUserMember._id); + + await request + .post(api('groups.kick')) + .set(credsOwner) + .send({ + roomId: groupTest._id, + userId: testUserMember._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }); + + it.skip('should kick user from group if not a member of the room but has the required permission', async () => { + await updatePermission('kick-user-from-any-p-room', ['admin']); + await inviteUser(testUserMember._id); + + await request .post(api('groups.kick')) .set(credentials) .send({ roomId: group._id, - userId: 'rocket.cat', + userId: testUserMember._id, }) .expect('Content-Type', 'application/json') - .expect(200) + .expect(200); + }); + + it("should return an error when the owner doesn't have the required permission", async () => { + await updatePermission('remove-user', ['admin', 'moderator']); + await inviteUser(testUserMember._id); + + await request + .post(api('groups.kick')) + .set(credsOwner) + .send({ + roomId: groupTest._id, + userId: testUserMember._id, + }) + .expect('Content-Type', 'application/json') + + .expect(400) .expect((res) => { - expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-not-allowed'); + }); + }); + + it('should return an error when trying to kick the last owner from a group', async () => { + await updatePermission('kick-user-from-any-p-room', ['admin']); + + await request + .post(api('groups.kick')) + .set(credentials) + .send({ + roomId: groupTest._id, + userId: testUserOwner._id, }) - .end(done); + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-you-are-last-owner'); + }); }); + + it('should return an error when trying to kick user that does not exist'); + it('should return an error when trying to kick user from a group that does not exist'); + it('should return an error when trying to kick user from a group that the user is not in the room'); }); describe('/groups.setDescription', () => { From 35363420f0ed20f1c3403669e1fffa0fd1730deb Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 12 Oct 2023 20:05:05 -0300 Subject: [PATCH 05/38] fix: Some HTTP requests sent by apps don't have their data parsed into JSON (#30560) --- .changeset/stale-masks-learn.md | 5 +++++ packages/server-fetch/src/parsers.ts | 30 +++++++++------------------- 2 files changed, 14 insertions(+), 21 deletions(-) create mode 100644 .changeset/stale-masks-learn.md diff --git a/.changeset/stale-masks-learn.md b/.changeset/stale-masks-learn.md new file mode 100644 index 000000000000..1523b02b0c95 --- /dev/null +++ b/.changeset/stale-masks-learn.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/server-fetch': patch +--- + +Fixed an issue where the payload of an HTTP request made by an app wouldn't be correctly encoded in some cases diff --git a/packages/server-fetch/src/parsers.ts b/packages/server-fetch/src/parsers.ts index 598ecbbd0e8e..ad0a44e96cfb 100644 --- a/packages/server-fetch/src/parsers.ts +++ b/packages/server-fetch/src/parsers.ts @@ -1,32 +1,20 @@ import type { ExtendedFetchOptions, FetchOptions, OriginalFetchOptions } from './types'; -function isPostOrPutOrDeleteWithBody(options?: ExtendedFetchOptions): boolean { - // No method === 'get' - if (!options?.method) { - return false; - } - const { method, body } = options; - const lowerMethod = method?.toLowerCase(); - return ['post', 'put', 'delete'].includes(lowerMethod) && body != null; -} - const jsonParser = (options: ExtendedFetchOptions) => { if (!options) { return {}; } - if (isPostOrPutOrDeleteWithBody(options)) { - try { - if (options && typeof options.body === 'object' && !Buffer.isBuffer(options.body)) { - options.body = JSON.stringify(options.body); - options.headers = { - 'Content-Type': 'application/json', - ...options.headers, - }; - } - } catch (e) { - // Body is not JSON, do nothing + try { + if (typeof options.body === 'object' && !Buffer.isBuffer(options.body)) { + options.body = JSON.stringify(options.body); + options.headers = { + ...options.headers, + 'Content-Type': 'application/json', // force content type to be json + }; } + } catch (e) { + // Body is not JSON, do nothing } return options as FetchOptions; From dd254a9bf5ff57fe0b99031e0d26fffd15fe2b2e Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Fri, 13 Oct 2023 11:33:59 -0300 Subject: [PATCH 06/38] fix: mobile ringing notification missing call id (#30614) --- .changeset/old-zoos-hang.md | 5 +++++ apps/meteor/server/services/video-conference/service.ts | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/old-zoos-hang.md diff --git a/.changeset/old-zoos-hang.md b/.changeset/old-zoos-hang.md new file mode 100644 index 000000000000..eb39a6c9d83c --- /dev/null +++ b/.changeset/old-zoos-hang.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix: mobile ringing notification missing call id diff --git a/apps/meteor/server/services/video-conference/service.ts b/apps/meteor/server/services/video-conference/service.ts index 77cdc1cbd8e0..c9079b0a2bfb 100644 --- a/apps/meteor/server/services/video-conference/service.ts +++ b/apps/meteor/server/services/video-conference/service.ts @@ -618,6 +618,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf caller: call.createdBy, avatar: getUserAvatarURL(call.createdBy.username), status: call.status, + callId: call._id, }, userId: calleeId, notId: PushNotification.getNotificationId(`${call.rid}|${call._id}`), From ae71e31624713efdb5c78084ab0fa8054fa2baa1 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Fri, 13 Oct 2023 12:29:57 -0300 Subject: [PATCH 07/38] refactor: `EditRoomInfo` to typescript (#28318) --- .../components/avatar/RoomAvatarEditor.tsx | 2 +- .../client/views/admin/rooms/EditRoom.tsx | 97 +--- .../views/hooks/roomActions/useDeleteRoom.tsx | 89 +++ .../Info/EditRoomInfo/EditChannel.js | 514 ------------------ .../Info/EditRoomInfo/EditChannelWithData.js | 14 - .../Info/EditRoomInfo/EditRoomInfo.tsx | 487 +++++++++++++++++ .../EditRoomInfo/EditRoomInfoWithData.tsx | 15 + .../contextualBar/Info/EditRoomInfo/index.ts | 2 +- .../EditRoomInfo/useEditRoomInitialValues.ts | 71 +++ .../EditRoomInfo/useEditRoomPermissions.ts | 77 +++ .../Info/hooks/actions/useRoomDelete.tsx | 45 -- .../Info/hooks/useRoomActions.ts | 10 +- .../contextualBar/info/TeamsInfoWithData.js | 24 +- 13 files changed, 765 insertions(+), 682 deletions(-) create mode 100644 apps/meteor/client/views/hooks/roomActions/useDeleteRoom.tsx delete mode 100644 apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js delete mode 100644 apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannelWithData.js create mode 100644 apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx create mode 100644 apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfoWithData.tsx create mode 100644 apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts create mode 100644 apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts delete mode 100644 apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomDelete.tsx diff --git a/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx b/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx index 32948eb42243..04b07e9cd627 100644 --- a/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx +++ b/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx @@ -71,7 +71,7 @@ const RoomAvatarEditor = ({ disabled = false, room, roomAvatar, onChangeAvatar } danger icon='trash' title={t('Accounts_SetDefaultAvatar')} - disabled={roomAvatar === null || isRoomFederated(room) || disabled} + disabled={!roomAvatar || isRoomFederated(room) || disabled} onClick={clickReset} /> diff --git a/apps/meteor/client/views/admin/rooms/EditRoom.tsx b/apps/meteor/client/views/admin/rooms/EditRoom.tsx index b12b5e3e1ab9..8130f2e9ec8b 100644 --- a/apps/meteor/client/views/admin/rooms/EditRoom.tsx +++ b/apps/meteor/client/views/admin/rooms/EditRoom.tsx @@ -13,18 +13,17 @@ import { TextAreaInput, } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, useRoute, usePermission, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import React, { useState, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { RoomSettingsEnum } from '../../../../definition/IRoomTypeConfig'; import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../components/Contextualbar'; -import GenericModal from '../../../components/GenericModal'; import RoomAvatarEditor from '../../../components/avatar/RoomAvatarEditor'; import { useEndpointAction } from '../../../hooks/useEndpointAction'; import { useForm } from '../../../hooks/useForm'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; -import DeleteTeamModalWithRooms from '../../teams/contextualBar/info/DeleteTeam'; +import { useDeleteRoom } from '../../hooks/roomActions/useDeleteRoom'; type EditRoomProps = { room: Pick; @@ -65,11 +64,6 @@ const getInitialValues = (room: Pick): EditRoomFormV const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => { const t = useTranslation(); - const [deleting, setDeleting] = useState(false); - - const setModal = useSetModal(); - const dispatchToastMessage = useToastMessageDispatch(); - const { values, handlers, hasUnsavedChanges, reset } = useForm(getInitialValues(room)); const [canViewName, canViewTopic, canViewAnnouncement, canViewArchived, canViewDescription, canViewType, canViewReadOnly] = @@ -119,9 +113,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => const changeArchivation = archived !== !!room.archived; - const roomsRoute = useRoute('admin-rooms'); - - const canDelete = usePermission(`delete-${room.t}`); + const { handleDelete, canDeleteRoom, isDeleting } = useDeleteRoom(room, { reload: onDelete }); const archiveSelector = room.archived ? 'unarchive' : 'archive'; const archiveMessage = room.archived ? 'Room_has_been_unarchived' : 'Room_has_been_archived'; @@ -161,61 +153,6 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => handleRoomType(roomType === 'p' ? 'c' : 'p'); }); - const deleteRoom = useEndpoint('POST', '/v1/rooms.delete'); - const deleteTeam = useEndpoint('POST', '/v1/teams.delete'); - - const handleDelete = useMutableCallback(() => { - const handleDeleteTeam = async (roomsToRemove: IRoom['_id'][]) => { - try { - setDeleting(true); - setModal(null); - await deleteTeam({ teamId: room.teamId as string, ...(roomsToRemove.length && { roomsToRemove }) }); - dispatchToastMessage({ type: 'success', message: t('Team_has_been_deleted') }); - roomsRoute.push({}); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - setDeleting(false); - } finally { - onDelete(); - } - }; - - if (room.teamMain) { - setModal( - setModal(null)} teamId={room.teamId as string} />, - ); - - return; - } - - const handleDeleteRoom = async (): Promise => { - try { - setDeleting(true); - setModal(null); - await deleteRoom({ roomId: room._id }); - dispatchToastMessage({ type: 'success', message: t('Room_has_been_deleted') }); - roomsRoute.push({}); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - setDeleting(false); - } finally { - onDelete(); - } - }; - - setModal( - setModal(null)} - onCancel={(): void => setModal(null)} - confirmText={t('Yes_delete_it')} - > - {t('Delete_Room_Warning')} - , - ); - }); - return ( <> e.preventDefault())}> @@ -227,7 +164,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Name')} - + {room.t !== 'd' && ( @@ -246,7 +183,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Topic')} - + )} @@ -281,7 +218,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Private')} - + {t('Just_invited_people_can_access_this_channel')} @@ -292,7 +229,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Read_only')} - + {t('Only_authorized_users_can_write_new_messages')} @@ -314,7 +251,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Room_archivation_state_true')} - + @@ -325,7 +262,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Default')} - + @@ -333,7 +270,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Favorite')} - + @@ -341,22 +278,22 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Featured')} - + - - - diff --git a/apps/meteor/client/views/hooks/roomActions/useDeleteRoom.tsx b/apps/meteor/client/views/hooks/roomActions/useDeleteRoom.tsx new file mode 100644 index 000000000000..be4728732284 --- /dev/null +++ b/apps/meteor/client/views/hooks/roomActions/useDeleteRoom.tsx @@ -0,0 +1,89 @@ +import type { IRoom, RoomAdminFieldsType } from '@rocket.chat/core-typings'; +import { isRoomFederated } from '@rocket.chat/core-typings'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useSetModal, useToastMessageDispatch, useRouter, usePermission, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useMutation } from '@tanstack/react-query'; +import React from 'react'; + +import GenericModal from '../../../components/GenericModal'; +import DeleteTeamModal from '../../teams/contextualBar/info/DeleteTeam'; + +export const useDeleteRoom = (room: IRoom | Pick, { reload }: { reload?: () => void } = {}) => { + const t = useTranslation(); + const router = useRouter(); + const setModal = useSetModal(); + const dispatchToastMessage = useToastMessageDispatch(); + const hasPermissionToDelete = usePermission(`delete-${room.t}`, room._id); + const canDeleteRoom = isRoomFederated(room) ? false : hasPermissionToDelete; + + const isAdminRoute = router.getRouteName() === 'admin-rooms'; + + const deleteRoomEndpoint = useEndpoint('POST', '/v1/rooms.delete'); + const deleteTeamEndpoint = useEndpoint('POST', '/v1/teams.delete'); + + const deleteRoomMutation = useMutation({ + mutationFn: deleteRoomEndpoint, + onSuccess: () => { + dispatchToastMessage({ type: 'success', message: t('Room_has_been_deleted') }); + if (isAdminRoute) { + return router.navigate('/admin/rooms'); + } + + return router.navigate('/home'); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + onSettled: () => { + setModal(null); + reload?.(); + }, + }); + + const deleteTeamMutation = useMutation({ + mutationFn: deleteTeamEndpoint, + onSuccess: () => { + dispatchToastMessage({ type: 'success', message: t('Team_has_been_deleted') }); + if (isAdminRoute) { + return router.navigate('/admin/rooms'); + } + + return router.navigate('/home'); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + onSettled: () => { + setModal(null); + reload?.(); + }, + }); + + const isDeleting = deleteTeamMutation.isLoading || deleteRoomMutation.isLoading; + + const handleDelete = useMutableCallback(() => { + const handleDeleteTeam = async (roomsToRemove: IRoom['_id'][]) => { + if (!room.teamId) { + return; + } + + deleteTeamMutation.mutateAsync({ teamId: room.teamId, ...(roomsToRemove.length && { roomsToRemove }) }); + }; + + if (room.teamMain && room.teamId) { + return setModal( setModal(null)} teamId={room.teamId} />); + } + + const handleDeleteRoom = async () => { + deleteRoomMutation.mutateAsync({ roomId: room._id }); + }; + + setModal( + setModal(null)} confirmText={t('Yes_delete_it')}> + {t('Delete_Room_Warning')} + , + ); + }); + + return { handleDelete, canDeleteRoom, isDeleting }; +}; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js deleted file mode 100644 index 22c84fbdebf1..000000000000 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js +++ /dev/null @@ -1,514 +0,0 @@ -import { isRoomFederated } from '@rocket.chat/core-typings'; -import { - Field, - TextInput, - PasswordInput, - ToggleSwitch, - MultiSelect, - Accordion, - Callout, - NumberInput, - FieldGroup, - FieldLabel, - FieldRow, - FieldHint, - Button, - ButtonGroup, - Box, - TextAreaInput, -} from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { - useSetModal, - useSetting, - usePermission, - useAtLeastOnePermission, - useRole, - useMethod, - useTranslation, - useRouter, -} from '@rocket.chat/ui-contexts'; -import React, { useCallback, useMemo, useRef } from 'react'; - -import { e2e } from '../../../../../../app/e2e/client/rocketchat.e2e'; -import { MessageTypesValues } from '../../../../../../app/lib/lib/MessageTypes'; -import { RoomSettingsEnum } from '../../../../../../definition/IRoomTypeConfig'; -import { - ContextualbarHeader, - ContextualbarBack, - ContextualbarTitle, - ContextualbarClose, - ContextualbarScrollableContent, - ContextualbarFooter, -} from '../../../../../components/Contextualbar'; -import GenericModal from '../../../../../components/GenericModal'; -import RawText from '../../../../../components/RawText'; -import RoomAvatarEditor from '../../../../../components/avatar/RoomAvatarEditor'; -import { useEndpointAction } from '../../../../../hooks/useEndpointAction'; -import { useForm } from '../../../../../hooks/useForm'; -import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator'; - -const typeMap = { - c: 'Channels', - p: 'Groups', - d: 'DMs', -}; - -const useInitialValues = (room, settings) => { - const { - t, - ro, - archived, - topic, - description, - announcement, - joinCodeRequired, - sysMes, - encrypted, - retention = {}, - reactWhenReadOnly, - } = room; - - const { retentionPolicyEnabled, maxAgeDefault } = settings; - - const retentionEnabledDefault = useSetting(`RetentionPolicy_AppliesTo${typeMap[room.t]}`); - const excludePinnedDefault = useSetting('RetentionPolicy_DoNotPrunePinned'); - const filesOnlyDefault = useSetting('RetentionPolicy_FilesOnly'); - - return useMemo( - () => ({ - roomName: t === 'd' ? room.usernames.join(' x ') : roomCoordinator.getRoomName(t, { type: t, ...room }), - roomType: t, - readOnly: !!ro, - reactWhenReadOnly, - archived: !!archived, - roomTopic: topic ?? '', - roomDescription: description ?? '', - roomAnnouncement: announcement ?? '', - roomAvatar: undefined, - joinCode: '', - joinCodeRequired: !!joinCodeRequired, - systemMessages: Array.isArray(sysMes) ? sysMes : [], - hideSysMes: !!sysMes?.length, - encrypted, - ...(retentionPolicyEnabled && { - retentionEnabled: retention.enabled ?? retentionEnabledDefault, - retentionOverrideGlobal: !!retention.overrideGlobal, - retentionMaxAge: Math.min(retention.maxAge, maxAgeDefault) || maxAgeDefault, - retentionExcludePinned: retention.excludePinned ?? excludePinnedDefault, - retentionFilesOnly: retention.filesOnly ?? filesOnlyDefault, - }), - }), - [ - announcement, - archived, - description, - excludePinnedDefault, - filesOnlyDefault, - joinCodeRequired, - maxAgeDefault, - retention.enabled, - retention.excludePinned, - retention.filesOnly, - retention.maxAge, - retention.overrideGlobal, - retentionEnabledDefault, - retentionPolicyEnabled, - ro, - room, - sysMes, - t, - topic, - encrypted, - reactWhenReadOnly, - ], - ); -}; - -const getCanChangeType = (room, canCreateChannel, canCreateGroup, isAdmin) => - (!room.default || isAdmin) && ((room.t === 'p' && canCreateChannel) || (room.t === 'c' && canCreateGroup)); - -function EditChannel({ room, onClickClose, onClickBack }) { - const t = useTranslation(); - - const setModal = useSetModal(); - - const retentionPolicyEnabled = useSetting('RetentionPolicy_Enabled'); - const maxAgeDefault = useSetting(`RetentionPolicy_MaxAge_${typeMap[room.t]}`) || 30; - - const saveData = useRef({}); - const router = useRouter(); - - const onChange = useCallback(({ initialValue, value, key }) => { - const { current } = saveData; - if (JSON.stringify(initialValue) !== JSON.stringify(value)) { - if (key === 'systemMessages' && value?.length > 0) { - current.hideSysMes = true; - } - current[key] = value; - } else { - delete current[key]; - } - }, []); - - const { values, handlers, hasUnsavedChanges, reset, commit } = useForm( - useInitialValues(room, { retentionPolicyEnabled, maxAgeDefault }), - onChange, - ); - - const sysMesOptions = useMemo(() => MessageTypesValues.map(({ key, i18nLabel }) => [key, t(i18nLabel)]), [t]); - - const { - roomName, - roomType, - readOnly, - encrypted, - roomAvatar, - archived, - roomTopic, - roomDescription, - roomAnnouncement, - reactWhenReadOnly, - joinCode, - joinCodeRequired, - systemMessages, - hideSysMes, - retentionEnabled, - retentionOverrideGlobal, - retentionMaxAge, - retentionExcludePinned, - retentionFilesOnly, - } = values; - - const { - handleJoinCode, - handleJoinCodeRequired, - handleSystemMessages, - handleEncrypted, - handleHideSysMes, - handleRoomName, - handleReadOnly, - handleArchived, - handleRoomAvatar, - handleReactWhenReadOnly, - handleRoomType, - handleRoomTopic, - handleRoomDescription, - handleRoomAnnouncement, - handleRetentionEnabled, - handleRetentionOverrideGlobal, - handleRetentionMaxAge, - handleRetentionExcludePinned, - handleRetentionFilesOnly, - } = handlers; - - const [ - canViewName, - canViewTopic, - canViewAnnouncement, - canViewArchived, - canViewDescription, - canViewType, - canViewReadOnly, - canViewHideSysMes, - canViewJoinCode, - canViewEncrypted, - ] = useMemo(() => { - const isAllowed = roomCoordinator.getRoomDirectives(room.t).allowRoomSettingChange || (() => {}); - return [ - isAllowed(room, RoomSettingsEnum.NAME), - isAllowed(room, RoomSettingsEnum.TOPIC), - isAllowed(room, RoomSettingsEnum.ANNOUNCEMENT), - isAllowed(room, RoomSettingsEnum.ARCHIVE_OR_UNARCHIVE), - isAllowed(room, RoomSettingsEnum.DESCRIPTION), - isAllowed(room, RoomSettingsEnum.TYPE), - isAllowed(room, RoomSettingsEnum.READ_ONLY), - isAllowed(room, RoomSettingsEnum.SYSTEM_MESSAGES), - isAllowed(room, RoomSettingsEnum.JOIN_CODE), - isAllowed(room, RoomSettingsEnum.REACT_WHEN_READ_ONLY), - isAllowed(room, RoomSettingsEnum.E2E), - ]; - }, [room]); - - const isAdmin = useRole('admin'); - - const canCreateChannel = usePermission('create-c'); - const canCreateGroup = usePermission('create-p'); - const canChangeType = getCanChangeType(room, canCreateChannel, canCreateGroup, isAdmin); - const canSetRo = usePermission('set-readonly', room._id); - const canSetReactWhenRo = usePermission('set-react-when-readonly', room._id); - const canEditRoomRetentionPolicy = usePermission('edit-room-retention-policy', room._id); - const canArchiveOrUnarchive = useAtLeastOnePermission( - useMemo(() => ['archive-room', 'unarchive-room'], []), - room._id, - ); - const canDelete = usePermission(`delete-${room.t}`); - const canToggleEncryption = usePermission('toggle-room-e2e-encryption', room._id) && (room.encrypted || e2e.isReady()); - - const changeArchivation = archived !== !!room.archived; - const archiveSelector = room.archived ? 'unarchive' : 'archive'; - const archiveMessage = room.archived ? 'Room_has_been_unarchived' : 'Room_has_been_archived'; - const saveAction = useEndpointAction('POST', '/v1/rooms.saveRoomSettings', { - successMessage: t('Room_updated_successfully'), - }); - const archiveAction = useEndpointAction('POST', '/v1/rooms.changeArchivationState', { successMessage: t(archiveMessage) }); - - const handleSave = useMutableCallback(async () => { - const { joinCodeRequired, hideSysMes, ...data } = saveData.current; - delete data.archived; - const save = () => - saveAction({ - rid: room._id, - ...data, - ...(joinCode && { joinCode: joinCodeRequired ? joinCode : '' }), - ...((data.systemMessages || !hideSysMes) && { - systemMessages: hideSysMes && systemMessages, - }), - }); - - const archive = () => archiveAction({ rid: room._id, action: archiveSelector }); - - await Promise.all([hasUnsavedChanges && save(), changeArchivation && archive()].filter(Boolean)); - saveData.current = {}; - commit(); - }); - - const deleteRoom = useMethod('eraseRoom'); - - const handleDelete = useMutableCallback(() => { - const onCancel = () => setModal(undefined); - const onConfirm = async () => { - await deleteRoom(room._id); - onCancel(); - router.navigate('/home'); - }; - - setModal( - - {t('Delete_Room_Warning')} - , - ); - }); - - const changeRoomType = useMutableCallback(() => { - handleRoomType(roomType === 'p' ? 'c' : 'p'); - }); - - const onChangeMaxAge = useMutableCallback((e) => { - handleRetentionMaxAge(Math.max(1, Number(e.currentTarget.value))); - }); - - const isFederated = useMemo(() => isRoomFederated(room), [room]); - - return ( - <> - - {onClickBack && } - {room.teamId ? t('edit-team') : t('edit-room')} - {onClickClose && } - - e.preventDefault())}> - - - - - {t('Name')} - - - - - {canViewDescription && ( - - {t('Description')} - - - - - )} - {canViewAnnouncement && ( - - {t('Announcement')} - - - - - )} - {canViewTopic && ( - - {t('Topic')} - - - - - )} - {canViewType && ( - - - {t('Private')} - - - - - {t('Teams_New_Private_Description_Enabled')} - - )} - {canViewReadOnly && ( - - - {t('Read_only')} - - - - - {t('Only_authorized_users_can_write_new_messages')} - - )} - {readOnly && ( - - - {t('React_when_read_only')} - - - - - {t('Only_authorized_users_can_react_to_messages')} - - )} - {canViewArchived && ( - - - {t('Room_archivation_state_true')} - - - - - - )} - {canViewJoinCode && ( - - - {t('Password_to_access')} - - - - - - - - - )} - {canViewHideSysMes && ( - - - {t('Hide_System_Messages')} - - - - - - - - - )} - {canViewEncrypted && ( - - - {t('Encrypted')} - - - - - - )} - {retentionPolicyEnabled && ( - - - - - - {t('RetentionPolicyRoom_Enabled')} - - - - - - - - {t('RetentionPolicyRoom_OverrideGlobal')} - - - - - - {retentionOverrideGlobal && ( - <> - - {t('RetentionPolicyRoom_ReadTheDocs')} - - - {t('RetentionPolicyRoom_MaxAge', { max: maxAgeDefault })} - - - - - - - {t('RetentionPolicyRoom_ExcludePinned')} - - - - - - - - {t('RetentionPolicyRoom_FilesOnly')} - - - - - - - )} - - - - )} - - - - - - - - - - - - ); -} - -export default EditChannel; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannelWithData.js b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannelWithData.js deleted file mode 100644 index e64dcc17562a..000000000000 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannelWithData.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -import { useRoom } from '../../../contexts/RoomContext'; -import { useRoomToolbox } from '../../../contexts/RoomToolboxContext'; -import EditChannel from './EditChannel'; - -function EditChannelWithData({ onClickBack }) { - const room = useRoom(); - const { closeTab } = useRoomToolbox(); - - return ; -} - -export default EditChannelWithData; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx new file mode 100644 index 000000000000..b2c552fd3d87 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx @@ -0,0 +1,487 @@ +import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; +import { isRoomFederated } from '@rocket.chat/core-typings'; +import type { SelectOption } from '@rocket.chat/fuselage'; +import { + Field, + FieldRow, + FieldLabel, + FieldHint, + TextInput, + PasswordInput, + ToggleSwitch, + MultiSelect, + Accordion, + Callout, + NumberInput, + FieldGroup, + Button, + ButtonGroup, + Box, + TextAreaInput, +} from '@rocket.chat/fuselage'; +import { useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import { useSetting, useTranslation, useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts'; +import React, { useMemo } from 'react'; +import { useForm, Controller } from 'react-hook-form'; + +import { MessageTypesValues } from '../../../../../../app/lib/lib/MessageTypes'; +import { + ContextualbarHeader, + ContextualbarBack, + ContextualbarTitle, + ContextualbarClose, + ContextualbarScrollableContent, + ContextualbarFooter, +} from '../../../../../components/Contextualbar'; +import RawText from '../../../../../components/RawText'; +import RoomAvatarEditor from '../../../../../components/avatar/RoomAvatarEditor'; +import { getDirtyFields } from '../../../../../lib/getDirtyFields'; +import { useDeleteRoom } from '../../../../hooks/roomActions/useDeleteRoom'; +import { useEditRoomInitialValues } from './useEditRoomInitialValues'; +import { useEditRoomPermissions } from './useEditRoomPermissions'; + +type EditRoomInfoProps = { + room: IRoomWithRetentionPolicy; + onClickClose: () => void; + onClickBack: () => void; +}; + +const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => { + const t = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + const isFederated = useMemo(() => isRoomFederated(room), [room]); + + const retentionPolicy = useSetting('RetentionPolicy_Enabled'); + const { handleDelete, canDeleteRoom } = useDeleteRoom(room); + const defaultValues = useEditRoomInitialValues(room); + + const { + watch, + reset, + control, + handleSubmit, + formState: { isDirty, dirtyFields, errors }, + } = useForm({ mode: 'onBlur', defaultValues }); + + const sysMesOptions: SelectOption[] = useMemo( + () => MessageTypesValues.map(({ key, i18nLabel }) => [key, t(i18nLabel as TranslationKey)]), + [t], + ); + + const { readOnly, archived, joinCodeRequired, hideSysMes, retentionEnabled, retentionMaxAge, retentionOverrideGlobal } = watch(); + + const { + canChangeType, + canSetReadOnly, + canSetReactWhenReadOnly, + canEditRoomRetentionPolicy, + canArchiveOrUnarchive, + canToggleEncryption, + canViewName, + canViewTopic, + canViewAnnouncement, + canViewArchived, + canViewDescription, + canViewType, + canViewReadOnly, + canViewHideSysMes, + canViewJoinCode, + canViewEncrypted, + } = useEditRoomPermissions(room); + + const changeArchiving = archived !== !!room.archived; + + const saveAction = useEndpoint('POST', '/v1/rooms.saveRoomSettings'); + const archiveAction = useEndpoint('POST', '/v1/rooms.changeArchivationState'); + + const handleUpdateRoomData = useMutableCallback(async ({ hideSysMes, ...formData }) => { + const data = getDirtyFields(formData, dirtyFields); + + try { + await saveAction({ + rid: room._id, + ...data, + ...(data.joinCode && { joinCode: joinCodeRequired ? data.joinCode : '' }), + ...((data.systemMessages || !hideSysMes) && { + systemMessages: hideSysMes && data.systemMessages, + }), + }); + + dispatchToastMessage({ type: 'success', message: t('Room_updated_successfully') }); + onClickClose(); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }); + + const handleArchive = useMutableCallback(async () => { + try { + await archiveAction({ rid: room._id, action: room.archived ? 'unarchive' : 'archive' }); + dispatchToastMessage({ type: 'success', message: room.archived ? t('Room_has_been_unarchived') : t('Room_has_been_archived') }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }); + + const handleSave = useMutableCallback(async (data) => { + await Promise.all([isDirty && handleUpdateRoomData(data), changeArchiving && handleArchive()].filter(Boolean)); + }); + + const formId = useUniqueId(); + const roomNameField = useUniqueId(); + const roomDescriptionField = useUniqueId(); + const roomAnnouncementField = useUniqueId(); + const roomTopicField = useUniqueId(); + const roomTypeField = useUniqueId(); + const readOnlyField = useUniqueId(); + const reactWhenReadOnlyField = useUniqueId(); + const archivedField = useUniqueId(); + const joinCodeRequiredField = useUniqueId(); + const hideSysMesField = useUniqueId(); + const encryptedField = useUniqueId(); + const retentionEnabledField = useUniqueId(); + const retentionOverrideGlobalField = useUniqueId(); + const retentionMaxAgeField = useUniqueId(); + const retentionExcludePinnedField = useUniqueId(); + const retentionFilesOnlyField = useUniqueId(); + + return ( + <> + + {onClickBack && } + {room.teamId ? t('edit-team') : t('edit-room')} + {onClickClose && } + + +
+ + } + /> + + + + + {t('Name')} + + + } + /> + + {errors.roomName && {errors.roomName.message}} + + {canViewDescription && ( + + {t('Description')} + + } + /> + + + )} + {canViewAnnouncement && ( + + {t('Announcement')} + + } + /> + + + )} + {canViewTopic && ( + + {t('Topic')} + + } + /> + + + )} + {canViewType && ( + + + {t('Private')} + + ( + onChange(value === 'p' ? 'c' : 'p')} + aria-describedby={`${roomTypeField}-hint`} + /> + )} + /> + + + {t('Teams_New_Private_Description_Enabled')} + + )} + {canViewReadOnly && ( + + + {t('Read_only')} + + ( + + )} + /> + + + {t('Only_authorized_users_can_write_new_messages')} + + )} + {readOnly && ( + + + {t('React_when_read_only')} + + ( + + )} + /> + + + {t('Only_authorized_users_can_react_to_messages')} + + )} + {canViewArchived && ( + + + {t('Room_archivation_state_true')} + + ( + + )} + /> + + + + )} + {canViewJoinCode && ( + + + {t('Password_to_access')} + + ( + + )} + /> + + + + } + /> + + + )} + {canViewHideSysMes && ( + + + {t('Hide_System_Messages')} + + ( + + )} + /> + + + + ( + + )} + /> + + + )} + {canViewEncrypted && ( + + + {t('Encrypted')} + + ( + + )} + /> + + + + )} + + {retentionPolicy && ( + + + + + + {t('RetentionPolicyRoom_Enabled')} + + ( + + )} + /> + + + + + + {t('RetentionPolicyRoom_OverrideGlobal')} + + ( + + )} + /> + + + + {retentionOverrideGlobal && ( + <> + + {t('RetentionPolicyRoom_ReadTheDocs')} + + + {t('RetentionPolicyRoom_MaxAge', { max: retentionMaxAge })} + + ( + onChange(Math.max(1, Number(currentValue)))} + /> + )} + /> + + + + + {t('RetentionPolicyRoom_ExcludePinned')} + + ( + + )} + /> + + + + + + {t('RetentionPolicyRoom_FilesOnly')} + + ( + + )} + /> + + + + + )} + + + + )} +
+
+ + + + + + + + + + + ); +}; + +export default EditRoomInfo; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfoWithData.tsx b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfoWithData.tsx new file mode 100644 index 000000000000..ad758c1bc8a6 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfoWithData.tsx @@ -0,0 +1,15 @@ +import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; +import React from 'react'; + +import { useRoom } from '../../../contexts/RoomContext'; +import { useRoomToolbox } from '../../../contexts/RoomToolboxContext'; +import EditRoomInfo from './EditRoomInfo'; + +const EditRoomInfoWithData = ({ onClickBack }: { onClickBack: () => void }) => { + const room = useRoom() as IRoomWithRetentionPolicy; + const { closeTab } = useRoomToolbox(); + + return ; +}; + +export default EditRoomInfoWithData; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/index.ts b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/index.ts index 4083ad9a958f..d8b31e17800c 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/index.ts +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/index.ts @@ -1 +1 @@ -export { default } from './EditChannelWithData'; +export { default } from './EditRoomInfoWithData'; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts new file mode 100644 index 000000000000..f36802bb9f56 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts @@ -0,0 +1,71 @@ +import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator'; + +const getPolicyRoomType = (roomType: IRoomWithRetentionPolicy['t']) => { + switch (roomType) { + case 'c': + return 'Channels'; + case 'p': + return 'Groups'; + case 'd': + return 'DMs'; + } +}; + +export const useEditRoomInitialValues = (room: IRoomWithRetentionPolicy) => { + const { t, ro, archived, topic, description, announcement, joinCodeRequired, sysMes, encrypted, retention, reactWhenReadOnly } = room; + + const retentionPolicyEnabled = useSetting('RetentionPolicy_Enabled'); + const maxAgeDefault = useSetting(`RetentionPolicy_MaxAge_${getPolicyRoomType(room.t)}`) || 30; + const retentionEnabledDefault = useSetting(`RetentionPolicy_AppliesTo${getPolicyRoomType(room.t)}`); + const excludePinnedDefault = useSetting('RetentionPolicy_DoNotPrunePinned'); + const filesOnlyDefault = useSetting('RetentionPolicy_FilesOnly'); + + return useMemo( + () => ({ + roomName: t === 'd' && room.usernames ? room.usernames.join(' x ') : roomCoordinator.getRoomName(t, room), + roomType: t, + readOnly: !!ro, + reactWhenReadOnly, + archived: !!archived, + roomTopic: topic ?? '', + roomDescription: description ?? '', + roomAnnouncement: announcement ?? '', + roomAvatar: undefined, + joinCode: '', + joinCodeRequired: !!joinCodeRequired, + systemMessages: Array.isArray(sysMes) ? sysMes : [], + hideSysMes: Array.isArray(sysMes) ? !!sysMes?.length : !!sysMes, + encrypted, + ...(retentionPolicyEnabled && { + retentionEnabled: retention?.enabled ?? retentionEnabledDefault, + retentionOverrideGlobal: !!retention?.overrideGlobal, + retentionMaxAge: Math.min(retention?.maxAge, maxAgeDefault) || maxAgeDefault, + retentionExcludePinned: retention?.excludePinned ?? excludePinnedDefault, + retentionFilesOnly: retention?.filesOnly ?? filesOnlyDefault, + }), + }), + [ + announcement, + archived, + description, + excludePinnedDefault, + filesOnlyDefault, + joinCodeRequired, + maxAgeDefault, + retention, + retentionEnabledDefault, + retentionPolicyEnabled, + ro, + room, + sysMes, + t, + topic, + encrypted, + reactWhenReadOnly, + ], + ); +}; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts new file mode 100644 index 000000000000..7b9e8c353941 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts @@ -0,0 +1,77 @@ +import type { IRoom, IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; +import { usePermission, useAtLeastOnePermission, useRole } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { e2e } from '../../../../../../app/e2e/client/rocketchat.e2e'; +import { RoomSettingsEnum } from '../../../../../../definition/IRoomTypeConfig'; +import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator'; + +const getCanChangeType = (room: IRoom | IRoomWithRetentionPolicy, canCreateChannel: boolean, canCreateGroup: boolean, isAdmin: boolean) => + (!room.default || isAdmin) && ((room.t === 'p' && canCreateChannel) || (room.t === 'c' && canCreateGroup)); + +export const useEditRoomPermissions = (room: IRoom | IRoomWithRetentionPolicy) => { + const isAdmin = useRole('admin'); + const canCreateChannel = usePermission('create-c'); + const canCreateGroup = usePermission('create-p'); + + const canChangeType = getCanChangeType(room, canCreateChannel, canCreateGroup, isAdmin); + const canSetReadOnly = usePermission('set-readonly', room._id); + const canSetReactWhenReadOnly = usePermission('set-react-when-readonly', room._id); + const canEditRoomRetentionPolicy = usePermission('edit-room-retention-policy', room._id); + const canArchiveOrUnarchive = useAtLeastOnePermission( + useMemo(() => ['archive-room', 'unarchive-room'], []), + room._id, + ); + const canToggleEncryption = usePermission('toggle-room-e2e-encryption', room._id) && (room.encrypted || e2e.isReady()); + + const [ + canViewName, + canViewTopic, + canViewAnnouncement, + canViewArchived, + canViewDescription, + canViewType, + canViewReadOnly, + canViewHideSysMes, + canViewJoinCode, + canViewEncrypted, + ] = useMemo(() => { + const isAllowed = + roomCoordinator.getRoomDirectives(room.t)?.allowRoomSettingChange || + (() => { + undefined; + }); + return [ + isAllowed(room, RoomSettingsEnum.NAME), + isAllowed(room, RoomSettingsEnum.TOPIC), + isAllowed(room, RoomSettingsEnum.ANNOUNCEMENT), + isAllowed(room, RoomSettingsEnum.ARCHIVE_OR_UNARCHIVE), + isAllowed(room, RoomSettingsEnum.DESCRIPTION), + isAllowed(room, RoomSettingsEnum.TYPE), + isAllowed(room, RoomSettingsEnum.READ_ONLY), + isAllowed(room, RoomSettingsEnum.SYSTEM_MESSAGES), + isAllowed(room, RoomSettingsEnum.JOIN_CODE), + isAllowed(room, RoomSettingsEnum.REACT_WHEN_READ_ONLY), + isAllowed(room, RoomSettingsEnum.E2E), + ]; + }, [room]); + + return { + canChangeType, + canSetReadOnly, + canSetReactWhenReadOnly, + canEditRoomRetentionPolicy, + canArchiveOrUnarchive, + canToggleEncryption, + canViewName, + canViewTopic, + canViewAnnouncement, + canViewArchived, + canViewDescription, + canViewType, + canViewReadOnly, + canViewHideSysMes, + canViewJoinCode, + canViewEncrypted, + }; +}; diff --git a/apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomDelete.tsx b/apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomDelete.tsx deleted file mode 100644 index b812e896bab9..000000000000 --- a/apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomDelete.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { isRoomFederated } from '@rocket.chat/core-typings'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, useTranslation, useEndpoint, usePermission, useRouter } from '@rocket.chat/ui-contexts'; -import React from 'react'; - -import GenericModal from '../../../../../../components/GenericModal'; - -// TODO: resetState for TeamsChannels -export const useRoomDelete = (room: IRoom, resetState?: () => void) => { - const t = useTranslation(); - const setModal = useSetModal(); - const dispatchToastMessage = useToastMessageDispatch(); - const router = useRouter(); - - const hasPermissionToDelete = usePermission(room.t === 'c' ? 'delete-c' : 'delete-p', room._id); - const canDelete = isRoomFederated(room) ? false : hasPermissionToDelete; - - const deleteRoom = useEndpoint('POST', room.t === 'c' ? '/v1/channels.delete' : '/v1/groups.delete'); - - const handleDelete = useMutableCallback(() => { - const onConfirm = async () => { - try { - await deleteRoom({ roomId: room._id }); - dispatchToastMessage({ type: 'success', message: t('Room_has_been_deleted') }); - if (resetState) { - return resetState(); - } - - router.navigate('/home'); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - setModal(null); - }; - - setModal( - setModal(null)} confirmText={t('Yes_delete_it')}> - {t('Delete_Room_Warning')} - , - ); - }); - - return canDelete ? handleDelete : null; -}; diff --git a/apps/meteor/client/views/room/contextualBar/Info/hooks/useRoomActions.ts b/apps/meteor/client/views/room/contextualBar/Info/hooks/useRoomActions.ts index d0ab03bd9ee7..638cd23b66ed 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/hooks/useRoomActions.ts +++ b/apps/meteor/client/views/room/contextualBar/Info/hooks/useRoomActions.ts @@ -1,10 +1,9 @@ -import { isRoomFederated } from '@rocket.chat/core-typings'; import type { IRoom } from '@rocket.chat/core-typings'; import { useTranslation } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; +import { useDeleteRoom } from '../../../../hooks/roomActions/useDeleteRoom'; import { useRoomConvertToTeam } from './actions/useRoomConvertToTeam'; -import { useRoomDelete } from './actions/useRoomDelete'; import { useRoomHide } from './actions/useRoomHide'; import { useRoomLeave } from './actions/useRoomLeave'; import { useRoomMoveToTeam } from './actions/useRoomMoveToTeam'; @@ -16,11 +15,10 @@ type RoomActions = { export const useRoomActions = (room: IRoom, { onClickEnterRoom, onClickEdit }: RoomActions, resetState?: () => void) => { const t = useTranslation(); - const isFederated = isRoomFederated(room); const handleHide = useRoomHide(room); const handleLeave = useRoomLeave(room); - const handleDelete = useRoomDelete(room, resetState); + const { handleDelete, canDeleteRoom } = useDeleteRoom(room, { reload: resetState }); const handleMoveToTeam = useRoomMoveToTeam(room); const handleConvertToTeam = useRoomConvertToTeam(room); @@ -40,7 +38,7 @@ export const useRoomActions = (room: IRoom, { onClickEnterRoom, onClickEdit }: R action: onClickEdit, }, }), - ...(!isFederated && + ...(canDeleteRoom && handleDelete && { delete: { label: t('Delete'), @@ -77,7 +75,7 @@ export const useRoomActions = (room: IRoom, { onClickEnterRoom, onClickEdit }: R }, }), }), - [onClickEdit, t, handleDelete, handleMoveToTeam, handleConvertToTeam, handleHide, handleLeave, onClickEnterRoom, isFederated], + [onClickEdit, t, handleDelete, handleMoveToTeam, handleConvertToTeam, handleHide, handleLeave, onClickEnterRoom, canDeleteRoom], ); return memoizedActions; diff --git a/apps/meteor/client/views/teams/contextualBar/info/TeamsInfoWithData.js b/apps/meteor/client/views/teams/contextualBar/info/TeamsInfoWithData.js index 0b72b3f164a6..f5cb4a44c5d2 100644 --- a/apps/meteor/client/views/teams/contextualBar/info/TeamsInfoWithData.js +++ b/apps/meteor/client/views/teams/contextualBar/info/TeamsInfoWithData.js @@ -16,10 +16,10 @@ import { GenericModalDoNotAskAgain } from '../../../../components/GenericModal'; import { useDontAskAgain } from '../../../../hooks/useDontAskAgain'; import { useEndpointAction } from '../../../../hooks/useEndpointAction'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; +import { useDeleteRoom } from '../../../hooks/roomActions/useDeleteRoom'; import { useRoom } from '../../../room/contexts/RoomContext'; import { useRoomToolbox } from '../../../room/contexts/RoomToolboxContext'; import ConvertToChannelModal from '../../ConvertToChannelModal'; -import DeleteTeamModal from './DeleteTeam'; import LeaveTeam from './LeaveTeam'; import TeamsInfo from './TeamsInfo'; @@ -56,7 +56,6 @@ const TeamsInfoWithLogic = ({ openEditing }) => { const setModal = useSetModal(); const closeModal = useMutableCallback(() => setModal()); - const deleteTeam = useEndpointAction('POST', '/v1/teams.delete'); const leaveTeam = useEndpointAction('POST', '/v1/teams.leave'); const convertTeamToChannel = useEndpointAction('POST', '/v1/teams.convertToChannel'); @@ -64,28 +63,11 @@ const TeamsInfoWithLogic = ({ openEditing }) => { const router = useRouter(); - const canDelete = usePermission('delete-team', room._id); const canEdit = usePermission('edit-team-channel', room._id); // const canLeave = usePermission('leave-team'); /* && room.cl !== false && joined */ - const onClickDelete = useMutableCallback(() => { - const onConfirm = async (deletedRooms) => { - const roomsToRemove = Array.isArray(deletedRooms) && deletedRooms.length > 0 ? deletedRooms : []; - - try { - await deleteTeam({ teamId: room.teamId, ...(roomsToRemove.length && { roomsToRemove }) }); - dispatchToastMessage({ type: 'success', message: t('Team_has_been_deleted') }); - router.navigate('/home'); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } finally { - closeModal(); - } - }; - - setModal(); - }); + const { handleDelete, canDeleteRoom } = useDeleteRoom(room); const onClickLeave = useMutableCallback(() => { const onConfirm = async (roomsLeft) => { @@ -174,7 +156,7 @@ const TeamsInfoWithLogic = ({ openEditing }) => { retentionPolicy={retentionPolicyEnabled && retentionPolicy} onClickEdit={canEdit && openEditing} onClickClose={closeTab} - onClickDelete={canDelete && onClickDelete} + onClickDelete={canDeleteRoom && handleDelete} onClickLeave={/* canLeave && */ onClickLeave} onClickHide={/* joined && */ handleHide} onClickViewChannels={onClickViewChannels} From 54d8ad439251808e26fd999286236ceb6e041aa1 Mon Sep 17 00:00:00 2001 From: Noach Magedman Date: Fri, 13 Oct 2023 19:20:55 +0300 Subject: [PATCH 08/38] fix: Improve FileProxy Handling, set Content-Type (#30427) Co-authored-by: Diego Sampaio <8591547+sampaiodiego@users.noreply.github.com> --- .changeset/tough-apples-turn.md | 5 ++++ .../app/file-upload/server/lib/FileUpload.ts | 27 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 .changeset/tough-apples-turn.md diff --git a/.changeset/tough-apples-turn.md b/.changeset/tough-apples-turn.md new file mode 100644 index 000000000000..056a0645186e --- /dev/null +++ b/.changeset/tough-apples-turn.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Forward headers when using proxy for file uploads diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.ts b/apps/meteor/app/file-upload/server/lib/FileUpload.ts index 8f929a17fe34..e512e5d09bfe 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.ts +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.ts @@ -562,7 +562,32 @@ export const FileUpload = { ) { res.setHeader('Content-Disposition', `${forceDownload ? 'attachment' : 'inline'}; filename="${encodeURI(fileName)}"`); - request.get(fileUrl, (fileRes) => fileRes.pipe(res)); + request.get(fileUrl, (fileRes) => { + if (fileRes.statusCode !== 200) { + res.setHeader('x-rc-proxyfile-status', String(fileRes.statusCode)); + res.setHeader('content-length', 0); + res.writeHead(500); + res.end(); + return; + } + + // eslint-disable-next-line prettier/prettier + const headersToProxy = [ + 'age', + 'cache-control', + 'content-length', + 'content-type', + 'date', + 'expired', + 'last-modified', + ]; + + headersToProxy.forEach((header) => { + fileRes.headers[header] && res.setHeader(header, String(fileRes.headers[header])); + }); + + fileRes.pipe(res); + }); }, generateJWTToFileUrls({ rid, userId, fileId }: { rid: string; userId: string; fileId: string }) { From 5da4636471037d563600553a287b821446c1f342 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Fri, 13 Oct 2023 18:31:06 -0300 Subject: [PATCH 09/38] refactor: Replace `useForm` in favor of RHF on `AppInstallPage` (#30634) --- .../views/marketplace/AppInstallPage.js | 74 ++++++++++--------- .../rocketchat-i18n/i18n/en.i18n.json | 1 + 2 files changed, 42 insertions(+), 33 deletions(-) diff --git a/apps/meteor/client/views/marketplace/AppInstallPage.js b/apps/meteor/client/views/marketplace/AppInstallPage.js index 77afd5361c88..f3b4f02b0c16 100644 --- a/apps/meteor/client/views/marketplace/AppInstallPage.js +++ b/apps/meteor/client/views/marketplace/AppInstallPage.js @@ -1,4 +1,5 @@ -import { Button, ButtonGroup, Icon, Field, FieldGroup, TextInput, Throbber } from '@rocket.chat/fuselage'; +import { Button, ButtonGroup, Icon, Field, FieldGroup, FieldLabel, FieldRow, TextInput } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useSetModal, useEndpoint, @@ -8,13 +9,13 @@ import { useRouter, useSearchParameter, } from '@rocket.chat/ui-contexts'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useState } from 'react'; +import { useForm, Controller } from 'react-hook-form'; import { AppClientOrchestratorInstance } from '../../../ee/client/apps/orchestrator'; import Page from '../../components/Page'; import { useAppsReload } from '../../contexts/hooks/useAppsReload'; import { useFileInput } from '../../hooks/useFileInput'; -import { useForm } from '../../hooks/useForm'; import AppPermissionsReviewModal from './AppPermissionsReviewModal'; import AppUpdateModal from './AppUpdateModal'; import AppInstallModal from './components/AppInstallModal/AppInstallModal'; @@ -48,22 +49,12 @@ function AppInstallPage() { const appCountQuery = useAppsCountQuery('private'); - const { values, handlers } = useForm({ - file: {}, - url: queryUrl, - }); + const { control, setValue, watch } = useForm({ defaultValues: { url: queryUrl || '' } }); + const { file, url } = watch(); - const { file, url } = values; + const canSave = !!url || !!file?.name; - const canSave = !!url || !!file.name; - - const { handleFile, handleUrl } = handlers; - - useEffect(() => { - queryUrl && handleUrl(queryUrl); - }, [queryUrl, handleUrl]); - - const [handleUploadButtonClick] = useFileInput(handleFile, 'app'); + const [handleUploadButtonClick] = useFileInput((value) => setValue('file', value), 'app'); const sendFile = async (permissionsGranted, appFile, appId) => { let app; @@ -200,35 +191,52 @@ function AppInstallPage() { }); }; + const urlField = useUniqueId(); + const fileField = useUniqueId(); + return ( - {t('App_Url_to_Install_From')} - - } /> - + {t('App_Url_to_Install_From')} + + ( + } {...field} /> + )} + /> + - {t('App_Url_to_Install_From_File')} - - - {t('Browse_Files')} - - } + {t('App_Url_to_Install_From_File')} + + ( + + {t('Browse_Files')} + + } + /> + )} /> - + diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 0a39692523eb..80ad18ad6743 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2611,6 +2611,7 @@ "Install_FxOs_done": "Great! You can now use Rocket.Chat via the icon on your homescreen. Have fun with Rocket.Chat!", "Install_FxOs_error": "Sorry, that did not work as intended! The following error appeared:", "Install_FxOs_follow_instructions": "Please confirm the app installation on your device (press \"Install\" when prompted).", + "Installing": "Installing", "Install_package": "Install package", "Installation": "Installation", "Installed": "Installed", From 169da3a0c80199fd499e9b32a2a189ef91dda45d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Sun, 15 Oct 2023 12:15:17 -0300 Subject: [PATCH 10/38] chore: improve `Tag` a11y link (#30636) --- .../RoomForeword/RoomForewordUsernameList.tsx | 5 +-- .../RoomForewordUsernameListItem.tsx | 15 ++++---- apps/meteor/package.json | 2 +- ee/packages/ui-theming/package.json | 2 +- ee/packages/ui-theming/src/palette.ts | 36 +++++++++---------- ee/packages/ui-theming/src/paletteDark.ts | 2 +- packages/fuselage-ui-kit/package.json | 2 +- packages/gazzodown/package.json | 2 +- packages/ui-client/package.json | 2 +- packages/ui-composer/package.json | 2 +- packages/ui-video-conf/package.json | 2 +- packages/uikit-playground/package.json | 2 +- yarn.lock | 24 ++++++------- 13 files changed, 48 insertions(+), 50 deletions(-) diff --git a/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameList.tsx b/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameList.tsx index b84a867d11d7..4677bb5e1ad3 100644 --- a/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameList.tsx +++ b/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameList.tsx @@ -1,4 +1,5 @@ import type { IUser } from '@rocket.chat/core-typings'; +import { Margins } from '@rocket.chat/fuselage'; import { useSetting } from '@rocket.chat/ui-contexts'; import type { VFC } from 'react'; import React from 'react'; @@ -11,7 +12,7 @@ type RoomForewordUsernameListProps = { usernames: Array = ({ usernames }) => { const useRealName = Boolean(useSetting('UI_Use_Real_Name')); return ( - <> + {usernames.map((username) => ( = ({ username useRealName={useRealName} /> ))} - + ); }; diff --git a/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameListItem.tsx b/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameListItem.tsx index fe148aa79d78..a0732b35d29d 100644 --- a/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameListItem.tsx +++ b/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameListItem.tsx @@ -1,5 +1,5 @@ import type { IUser } from '@rocket.chat/core-typings'; -import { Box, Icon, Tag, Skeleton } from '@rocket.chat/fuselage'; +import { Icon, Tag, Skeleton } from '@rocket.chat/fuselage'; import type { VFC } from 'react'; import React from 'react'; @@ -12,18 +12,15 @@ type RoomForewordUsernameListItemProps = { useRealName: boolean; }; -// TODO: Improve `Tag` a11y to be used as a link const RoomForewordUsernameListItem: VFC = ({ username, href, useRealName }) => { const { data, isLoading, isError } = useUserInfoQuery({ username }); return ( - - } className='mention-link' data-username={username} large> - {isLoading && } - {!isLoading && isError && username} - {!isLoading && !isError && getUserDisplayName(data?.user?.name, username, useRealName)} - - + } data-username={username} large href={href}> + {isLoading && } + {!isLoading && isError && username} + {!isLoading && !isError && getUserDisplayName(data?.user?.name, username, useRealName)} + ); }; diff --git a/apps/meteor/package.json b/apps/meteor/package.json index c1014a749158..3ee3366f47dd 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -236,7 +236,7 @@ "@rocket.chat/favicon": "workspace:^", "@rocket.chat/forked-matrix-appservice-bridge": "^4.0.1", "@rocket.chat/forked-matrix-bot-sdk": "^0.6.0-beta.2", - "@rocket.chat/fuselage": "^0.32.2", + "@rocket.chat/fuselage": "^0.34.0", "@rocket.chat/fuselage-hooks": "^0.32.1", "@rocket.chat/fuselage-polyfills": "next", "@rocket.chat/fuselage-toastbar": "next", diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index 9508e2b8e41e..11aa5fd57ff8 100644 --- a/ee/packages/ui-theming/package.json +++ b/ee/packages/ui-theming/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@rocket.chat/css-in-js": "next", - "@rocket.chat/fuselage": "^0.32.2", + "@rocket.chat/fuselage": "^0.34.0", "@rocket.chat/fuselage-hooks": "^0.32.1", "@rocket.chat/icons": "^0.32.0", "@rocket.chat/ui-contexts": "workspace:~", diff --git a/ee/packages/ui-theming/src/palette.ts b/ee/packages/ui-theming/src/palette.ts index 39f8d3f2bfed..5d207d63bc21 100644 --- a/ee/packages/ui-theming/src/palette.ts +++ b/ee/packages/ui-theming/src/palette.ts @@ -46,8 +46,8 @@ export const palette = [ { name: 'font-white', token: 'white', color: '#FFFFFF' }, { name: 'font-disabled', token: 'N500', color: '#CBCED1' }, { name: 'font-annotation', token: 'N600', color: '#9EA2A8' }, - { name: 'font-hint', token: 'N700', color: '#6C727A' }, - { name: 'font-secondary-info', token: 'N700', color: '#6C727A' }, + { name: 'font-hint', token: 'N700', color: '#6C737A' }, + { name: 'font-secondary-info', token: 'N700', color: '#6C737A' }, { name: 'font-default', token: 'N800', color: '#2F343D' }, { name: 'font-titles-labels', token: 'N900', color: '#1F2329' }, { name: 'font-info', token: 'P600', color: '#095AD2' }, @@ -76,7 +76,7 @@ export const palette = [ { name: 'status-font-on-info', token: 'P600', color: '#095AD2' }, { name: 'status-font-on-success', token: 'S800', color: '#148660' }, { name: 'status-font-on-danger', token: 'D800', color: '#9B1325' }, - { name: 'status-font-on-warning', token: 'W900', color: '#B88D00' }, + { name: 'status-font-on-warning', token: 'W900', color: '#8E6300' }, { name: 'status-font-on-warning-2', token: 'N800', color: '#2F343D' }, { name: 'status-font-on-service-1', token: 'S1-800', color: '#974809' }, { name: 'status-font-on-service-2 ', token: 'S2-600', color: '#7F1B9F' }, @@ -88,22 +88,22 @@ export const palette = [ description: 'Badge Background', list: [ { name: 'badge-background-level-0', token: '', color: '#E4E7EA' }, - { name: 'badge-background-level-1', token: 'N700', color: '#6C727A' }, - { name: 'badge-background-level-2', token: '', color: '#1D74F5' }, + { name: 'badge-background-level-1', token: 'N700', color: '#6C737A' }, + { name: 'badge-background-level-2', token: '', color: '#156FF5' }, { name: 'badge-background-level-3', token: '', color: '#F38C39' }, - { name: 'badge-background-level-4', token: '', color: '#F5455C' }, + { name: 'badge-background-level-4', token: '', color: '#EC0D2A' }, ], }, { category: 'Status Bullet', description: 'Used to show user status', list: [ - { name: 'status-bullet-online', token: '', color: '#158D65' }, + { name: 'status-bullet-online', token: '', color: '#148660' }, { name: 'status-bullet-away', token: '', color: '#AC892F' }, - { name: 'status-bullet-busy', token: '', color: '#DA1F37' }, + { name: 'status-bullet-busy', token: '', color: '#D40C26' }, { name: 'status-bullet-disabled', token: '', color: '#F38C39' }, - { name: 'status-bullet-offline', token: '', color: '#AC892F' }, - { name: 'status-bullet-loading', token: '', color: '#9ea2a8' }, + { name: 'status-bullet-offline', token: '', color: '#6C737A' }, + { name: 'status-bullet-loading', token: '', color: '#6C737A' }, ], }, { @@ -122,7 +122,7 @@ export const palette = [ list: [ { name: 'button-background-primary-default', token: 'P500', color: '#156FF5' }, { name: 'button-background-primary-hover', token: 'P600', color: '#095AD2' }, - { name: 'button-background-primary-press', token: 'P700', color: '#095AD2' }, + { name: 'button-background-primary-press', token: 'P700', color: '#10529E' }, { name: 'button-background-primary-focus', token: 'P500', color: '#156FF5' }, { name: 'button-background-primary-keyfocus', token: 'P500', color: '#156FF5' }, { name: 'button-background-primary-disabled', token: 'P200', color: '#D1EBFE' }, @@ -133,7 +133,7 @@ export const palette = [ list: [ { name: 'button-background-secondary-default', token: 'N400', color: '#E4E7EA' }, { name: 'button-background-secondary-hover', token: 'N500', color: '#CBCED1' }, - { name: 'button-background-secondary-press', token: 'N600', color: '#CBCED1' }, + { name: 'button-background-secondary-press', token: 'N600', color: '#9EA2A8' }, { name: 'button-background-secondary-focus', token: 'N400', color: '#E4E7EA' }, { name: 'button-background-secondary-keyfocus', token: 'N400', color: '#E4E7EA' }, { name: 'button-background-secondary-disabled', token: 'N300', color: '#EEEFF1' }, @@ -144,7 +144,7 @@ export const palette = [ list: [ { name: 'button-background-secondary-danger-default', token: 'N400', color: '#E4E7EA' }, { name: 'button-background-secondary-danger-hover', token: 'N500', color: '#CBCED1' }, - { name: 'button-background-secondary-danger-press', token: 'N600', color: '#CBCED1' }, + { name: 'button-background-secondary-danger-press', token: 'N600', color: '#9EA2A8' }, { name: 'button-background-secondary-danger-focus', token: 'N400', color: '#E4E7EA' }, { name: 'button-background-secondary-danger-keyfocus', token: 'N400', color: '#E4E7EA' }, { name: 'button-background-secondary-danger-disabled', token: 'N300', color: '#EEEFF1' }, @@ -164,11 +164,11 @@ export const palette = [ { description: 'Success Background', list: [ - { name: 'button-background-success-default', token: '', color: '#158D65' }, + { name: 'button-background-success-default', token: '', color: '#148660' }, { name: 'button-background-success-hover', token: 'S900', color: '#106D4F' }, { name: 'button-background-success-press', token: 'S1000', color: '#0D5940' }, - { name: 'button-background-success-focus', token: '', color: '#158D65' }, - { name: 'button-background-success-keyfocus', token: '', color: '#158D65' }, + { name: 'button-background-success-focus', token: '', color: '#148660' }, + { name: 'button-background-success-keyfocus', token: '', color: '#148660' }, { name: 'button-background-success-disabled', token: 'S200', color: '#C0F6E4' }, ], }, @@ -179,7 +179,7 @@ export const palette = [ { name: 'button-font-on-primary-disabled', token: 'white', color: '#FFFFFF' }, { name: 'button-font-on-secondary', token: 'N900', color: '#1F2329' }, { name: 'button-font-on-secondary-disabled', token: 'N600', color: '#CBCED1' }, - { name: 'button-font-on-secondary-danger', token: 'D900', color: '#BB0B21' }, + { name: 'button-font-on-secondary-danger', token: '', color: '#BB0B21' }, { name: 'button-font-on-secondary-danger-disabled', token: 'D300', @@ -187,7 +187,7 @@ export const palette = [ }, { name: 'button-font-on-danger', token: 'white', color: '#FFFFFF' }, { name: 'button-font-on-danger-disabled', token: 'white', color: '#FFFFFF' }, - { name: 'button-font-on-success', token: '', color: '#EBECEF' }, + { name: 'button-font-on-success', token: '', color: '#FFFFFF' }, { name: 'button-font-on-success-disabled', token: 'white', color: '#FFFFFF' }, ], }, diff --git a/ee/packages/ui-theming/src/paletteDark.ts b/ee/packages/ui-theming/src/paletteDark.ts index cdb60efffeac..89ac7817be42 100644 --- a/ee/packages/ui-theming/src/paletteDark.ts +++ b/ee/packages/ui-theming/src/paletteDark.ts @@ -9,7 +9,7 @@ export const palette = [ { name: 'stroke-dark', token: 'N600', color: '#9EA2A8' }, { name: 'stroke-extra-dark', token: 'N400', color: '#CBCED1' }, { name: 'stroke-extra-light-highlight', token: '', color: '#87CBFC' }, - { name: 'stroke-highlight', token: '', color: '#3976D1' }, + { name: 'stroke-highlight', token: '', color: '#6292DA' }, { name: 'stroke-extra-light-error', token: '', color: '#F49AA6' }, { name: 'stroke-error', token: '', color: '#BB3E4E' }, ], diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 83ab677e2a9f..4555216c2d44 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -56,7 +56,7 @@ "devDependencies": { "@rocket.chat/apps-engine": "1.41.0-alpha.290", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.32.2", + "@rocket.chat/fuselage": "^0.34.0", "@rocket.chat/fuselage-hooks": "^0.32.1", "@rocket.chat/fuselage-polyfills": "next", "@rocket.chat/icons": "^0.32.0", diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 89136a296f9d..e891e5677c75 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.9", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "next", - "@rocket.chat/fuselage": "^0.32.2", + "@rocket.chat/fuselage": "^0.34.0", "@rocket.chat/fuselage-tokens": "next", "@rocket.chat/message-parser": "next", "@rocket.chat/styled": "next", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index fa227575ccc2..f8ef2d3a1e93 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -5,7 +5,7 @@ "devDependencies": { "@babel/core": "~7.22.9", "@rocket.chat/css-in-js": "next", - "@rocket.chat/fuselage": "^0.32.2", + "@rocket.chat/fuselage": "^0.34.0", "@rocket.chat/fuselage-hooks": "^0.32.1", "@rocket.chat/icons": "^0.32.0", "@rocket.chat/mock-providers": "workspace:^", diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index 1d5d478e17cd..b5c804b4a2ad 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -5,7 +5,7 @@ "devDependencies": { "@babel/core": "~7.22.9", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.32.2", + "@rocket.chat/fuselage": "^0.34.0", "@rocket.chat/icons": "^0.32.0", "@storybook/addon-actions": "~6.5.16", "@storybook/addon-docs": "~6.5.16", diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index d253b61cc1b0..7ca7b1d86140 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.9", "@rocket.chat/css-in-js": "next", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.32.2", + "@rocket.chat/fuselage": "^0.34.0", "@rocket.chat/fuselage-hooks": "^0.32.1", "@rocket.chat/icons": "^0.32.0", "@rocket.chat/styled": "next", diff --git a/packages/uikit-playground/package.json b/packages/uikit-playground/package.json index 5e8e3bf2cc5c..5a8b1276defb 100644 --- a/packages/uikit-playground/package.json +++ b/packages/uikit-playground/package.json @@ -15,7 +15,7 @@ "@codemirror/tooltip": "^0.19.16", "@lezer/highlight": "^1.1.6", "@rocket.chat/css-in-js": "next", - "@rocket.chat/fuselage": "^0.32.2", + "@rocket.chat/fuselage": "^0.34.0", "@rocket.chat/fuselage-hooks": "^0.32.1", "@rocket.chat/fuselage-polyfills": "next", "@rocket.chat/fuselage-tokens": "next", diff --git a/yarn.lock b/yarn.lock index 7d75c388dc29..b4e4af200f30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8273,7 +8273,7 @@ __metadata: dependencies: "@rocket.chat/apps-engine": 1.41.0-alpha.290 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.32.2 + "@rocket.chat/fuselage": ^0.34.0 "@rocket.chat/fuselage-hooks": ^0.32.1 "@rocket.chat/fuselage-polyfills": next "@rocket.chat/gazzodown": "workspace:^" @@ -8322,9 +8322,9 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/fuselage@npm:^0.32.2": - version: 0.32.2 - resolution: "@rocket.chat/fuselage@npm:0.32.2" +"@rocket.chat/fuselage@npm:^0.34.0": + version: 0.34.0 + resolution: "@rocket.chat/fuselage@npm:0.34.0" dependencies: "@rocket.chat/css-in-js": ^0.31.25 "@rocket.chat/css-supports": ^0.31.25 @@ -8342,7 +8342,7 @@ __metadata: react: ^17.0.2 react-dom: ^17.0.2 react-virtuoso: 1.2.4 - checksum: 28e80385961b090c71d0897c22c3c799ca05d30285456d96d3ca5ff2a1a4ba02362644084e611bd3f2a376acdf4c2e75180b8aee196a63969a7d6559abd73d79 + checksum: 72cd1dd7ef13cc3b69fadac5c064a45cd2b65b8a221cde2e8149fa873ac6de89648c677caedb10979e5cf08d39b79f1d7a30caa6378bdeeb873414c7fbac5e6e languageName: node linkType: hard @@ -8353,7 +8353,7 @@ __metadata: "@babel/core": ~7.22.9 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": next - "@rocket.chat/fuselage": ^0.32.2 + "@rocket.chat/fuselage": ^0.34.0 "@rocket.chat/fuselage-tokens": next "@rocket.chat/message-parser": next "@rocket.chat/styled": next @@ -8706,7 +8706,7 @@ __metadata: "@rocket.chat/favicon": "workspace:^" "@rocket.chat/forked-matrix-appservice-bridge": ^4.0.1 "@rocket.chat/forked-matrix-bot-sdk": ^0.6.0-beta.2 - "@rocket.chat/fuselage": ^0.32.2 + "@rocket.chat/fuselage": ^0.34.0 "@rocket.chat/fuselage-hooks": ^0.32.1 "@rocket.chat/fuselage-polyfills": next "@rocket.chat/fuselage-toastbar": next @@ -9569,7 +9569,7 @@ __metadata: dependencies: "@babel/core": ~7.22.9 "@rocket.chat/css-in-js": next - "@rocket.chat/fuselage": ^0.32.2 + "@rocket.chat/fuselage": ^0.34.0 "@rocket.chat/fuselage-hooks": ^0.32.1 "@rocket.chat/icons": ^0.32.0 "@rocket.chat/mock-providers": "workspace:^" @@ -9620,7 +9620,7 @@ __metadata: dependencies: "@babel/core": ~7.22.9 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.32.2 + "@rocket.chat/fuselage": ^0.34.0 "@rocket.chat/icons": ^0.32.0 "@storybook/addon-actions": ~6.5.16 "@storybook/addon-docs": ~6.5.16 @@ -9691,7 +9691,7 @@ __metadata: resolution: "@rocket.chat/ui-theming@workspace:ee/packages/ui-theming" dependencies: "@rocket.chat/css-in-js": next - "@rocket.chat/fuselage": ^0.32.2 + "@rocket.chat/fuselage": ^0.34.0 "@rocket.chat/fuselage-hooks": ^0.32.1 "@rocket.chat/icons": ^0.32.0 "@rocket.chat/ui-contexts": "workspace:~" @@ -9734,7 +9734,7 @@ __metadata: "@rocket.chat/css-in-js": next "@rocket.chat/emitter": next "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.32.2 + "@rocket.chat/fuselage": ^0.34.0 "@rocket.chat/fuselage-hooks": ^0.32.1 "@rocket.chat/icons": ^0.32.0 "@rocket.chat/styled": next @@ -9777,7 +9777,7 @@ __metadata: "@codemirror/tooltip": ^0.19.16 "@lezer/highlight": ^1.1.6 "@rocket.chat/css-in-js": next - "@rocket.chat/fuselage": ^0.32.2 + "@rocket.chat/fuselage": ^0.34.0 "@rocket.chat/fuselage-hooks": ^0.32.1 "@rocket.chat/fuselage-polyfills": next "@rocket.chat/fuselage-tokens": next From bbbbdacc51149c3f6651a1cf61bba9d60773fbf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Mon, 16 Oct 2023 14:25:30 -0300 Subject: [PATCH 11/38] feat: add tooltip to badge mentions (#30590) --- .../RoomList/SideBarItemTemplateWithData.tsx | 28 ++++++++++++++++++- .../rocketchat-i18n/i18n/en.i18n.json | 8 ++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx index b96c54d4c955..f275ff2800d8 100644 --- a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx +++ b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx @@ -33,6 +33,30 @@ const getMessage = (room: IRoom, lastMessage: IMessage | undefined, t: ReturnTyp return `${lastMessage.u.name || lastMessage.u.username}: ${normalizeSidebarMessage(lastMessage, t)}`; }; +const getBadgeTitle = ( + userMentions: number, + threadUnread: number, + groupMentions: number, + unread: number, + t: ReturnType, +) => { + const title = [] as string[]; + if (userMentions) { + title.push(t('mentions_counter', { count: userMentions })); + } + if (threadUnread) { + title.push(t('threads_counter', { count: threadUnread })); + } + if (groupMentions) { + title.push(t('group_mentions_counter', { count: groupMentions })); + } + const count = unread - userMentions - groupMentions; + if (count > 0) { + title.push(t('unread_messages_counter', { count })); + } + return title.join(', '); +}; + type RoomListRowProps = { extended: boolean; t: ReturnType; @@ -137,10 +161,12 @@ function SideBarItemTemplateWithData({ const isUnread = unread > 0 || threadUnread; const showBadge = !hideUnreadStatus || (!hideMentionStatus && (Boolean(userMentions) || tunreadUser.length > 0)); + const badgeTitle = getBadgeTitle(userMentions, tunread.length, groupMentions, unread, t); + const badges = ( {showBadge && isUnread && ( - + {unread + tunread?.length} )} diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 80ad18ad6743..001cdf080f7b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -6086,6 +6086,14 @@ "Filter_by_room": "Filter by room type", "Filter_by_visibility": "Filter by visibility", "Theme_Appearence": "Theme Appearence", + "mentions_counter": "{{count}} mention", + "mentions_counter_plural": "{{count}} mentions", + "threads_counter": "{{count}} unread threaded message", + "threads_counter_plural": "{{count}} unread threaded messages", + "group_mentions_counter": "{{count}} group mention", + "group_mentions_counter_plural": "{{count}} group mentions", + "unread_messages_counter": "{{count}} unread message", + "unread_messages_counter_plural": "{{count}} unread messages", "Premium": "Premium", "Premium_capability": "Premium capability" } \ No newline at end of file From b22da641303e3b22571519125de68311756ebf17 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 16 Oct 2023 11:50:45 -0600 Subject: [PATCH 12/38] refactor: Livechat functions to Typescript (#30631) --- .../app/apps/server/bridges/livechat.ts | 2 +- .../imports/server/rest/departments.ts | 10 +- .../meteor/app/livechat/server/api/v1/room.ts | 13 +- .../app/livechat/server/api/v1/videoCall.ts | 2 +- .../app/livechat/server/api/v1/visitor.ts | 2 +- .../server/hooks/saveContactLastChat.ts | 2 +- .../app/livechat/server/lib/Livechat.js | 125 +----------------- .../app/livechat/server/lib/LivechatTyped.ts | 111 +++++++++++++++- apps/meteor/lib/callbacks.ts | 3 +- .../server/services/omnichannel/service.ts | 2 +- packages/core-services/src/Events.ts | 4 +- packages/core-typings/src/ILivechatVisitor.ts | 2 +- .../src/models/ILivechatDepartmentModel.ts | 2 + 13 files changed, 137 insertions(+), 143 deletions(-) diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 76a0545c8801..0ace08bb8446 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -288,7 +288,7 @@ export class AppLivechatBridge extends LivechatBridge { throw new Error('Could not get the message converter to process livechat room messages'); } - const livechatMessages = await Livechat.getRoomMessages({ rid: roomId }); + const livechatMessages = await LivechatTyped.getRoomMessages({ rid: roomId }); return Promise.all(livechatMessages.map((message) => messageConverter.convertMessage(message) as Promise)); } diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index 6540b67d79aa..095baefaa294 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -17,6 +17,7 @@ import { } from '../../../server/api/lib/departments'; import { DepartmentHelper } from '../../../server/lib/Departments'; import { Livechat } from '../../../server/lib/Livechat'; +import { Livechat as LivechatTs } from '../../../server/lib/LivechatTyped'; API.v1.addRoute( 'livechat/department', @@ -192,7 +193,7 @@ API.v1.addRoute( }, { async post() { - await Livechat.archiveDepartment(this.urlParams._id); + await LivechatTs.archiveDepartment(this.urlParams._id); return API.v1.success(); }, @@ -207,11 +208,8 @@ API.v1.addRoute( }, { async post() { - if (await Livechat.unarchiveDepartment(this.urlParams._id)) { - return API.v1.success(); - } - - return API.v1.failure(); + await LivechatTs.unarchiveDepartment(this.urlParams._id); + return API.v1.success(); }, }, ); diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 86629e636bf8..8f6151797463 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -18,7 +18,7 @@ import { callbacks } from '../../../../../lib/callbacks'; import { i18n } from '../../../../../server/lib/i18n'; import { API } from '../../../../api/server'; import { isWidget } from '../../../../api/server/helpers/isWidget'; -import { canAccessRoomAsync } from '../../../../authorization/server'; +import { canAccessRoomAsync, roomAccessAttributes } from '../../../../authorization/server'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { addUserToRoom } from '../../../../lib/server/functions/addUserToRoom'; import { settings as rcSettings } from '../../../../settings/server'; @@ -352,7 +352,12 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/room.visitor', - { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isPUTLivechatRoomVisitorParams, deprecationVersion: '7.0.0' }, + { + authRequired: true, + permissionsRequired: ['change-livechat-room-visitor'], + validateParams: isPUTLivechatRoomVisitorParams, + deprecationVersion: '7.0.0', + }, { async put() { // This endpoint is deprecated and will be removed in future versions. @@ -363,7 +368,7 @@ API.v1.addRoute( throw new Error('invalid-visitor'); } - const room = await LivechatRooms.findOneById(rid, { _id: 1, v: 1 }); // TODO: check _id + const room = await LivechatRooms.findOneById(rid, { projection: { ...roomAccessAttributes, _id: 1, t: 1, v: 1 } }); // TODO: check _id if (!room) { throw new Error('invalid-room'); } @@ -373,7 +378,7 @@ API.v1.addRoute( throw new Error('invalid-room-visitor'); } - const roomAfterChange = await Livechat.changeRoomVisitor(this.userId, rid, visitor); + const roomAfterChange = await LivechatTyped.changeRoomVisitor(this.userId, room, visitor); if (!roomAfterChange) { return API.v1.failure(); diff --git a/apps/meteor/app/livechat/server/api/v1/videoCall.ts b/apps/meteor/app/livechat/server/api/v1/videoCall.ts index 5ce0ddc4ca37..52cd8738bec9 100644 --- a/apps/meteor/app/livechat/server/api/v1/videoCall.ts +++ b/apps/meteor/app/livechat/server/api/v1/videoCall.ts @@ -6,7 +6,7 @@ import { i18n } from '../../../../../server/lib/i18n'; import { API } from '../../../../api/server'; import { canSendMessageAsync } from '../../../../authorization/server/functions/canSendMessage'; import { settings as rcSettings } from '../../../../settings/server'; -import { Livechat } from '../../lib/Livechat'; +import { Livechat } from '../../lib/LivechatTyped'; import { settings } from '../lib/livechat'; API.v1.addRoute( diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index ae9d1ea4fd83..84f7b96e155d 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -174,7 +174,7 @@ API.v1.addRoute('livechat/visitor.callStatus', { if (!guest) { throw new Meteor.Error('invalid-token'); } - await Livechat.updateCallStatus(callId, rid, callStatus, guest); + await LivechatTyped.updateCallStatus(callId, rid, callStatus, guest); return API.v1.success({ token, callStatus }); }, }); diff --git a/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts b/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts index 7b4d9b89f14c..6f42a910417d 100644 --- a/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts +++ b/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts @@ -1,7 +1,7 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { callbacks } from '../../../../lib/callbacks'; -import { Livechat } from '../lib/Livechat'; +import { Livechat } from '../lib/LivechatTyped'; callbacks.add( 'livechat.newRoom', diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index c560f3dd7aa7..e1d6626c7ddb 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -4,7 +4,7 @@ import dns from 'dns'; import util from 'util'; -import { Message, VideoConf, api } from '@rocket.chat/core-services'; +import { Message } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; import { LivechatVisitors, @@ -30,7 +30,6 @@ import { trim } from '../../../../lib/utils/stringUtils'; import { i18n } from '../../../../server/lib/i18n'; import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; import { removeUserFromRolesAsync } from '../../../../server/lib/roles/removeUserFromRoles'; -import { canAccessRoomAsync, roomAccessAttributes } from '../../../authorization/server'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { FileUpload } from '../../../file-upload/server'; import { deleteMessage } from '../../../lib/server/functions/deleteMessage'; @@ -718,40 +717,6 @@ export const Livechat = { return ret; }, - async unarchiveDepartment(_id) { - check(_id, String); - - const department = await LivechatDepartmentRaw.findOneById(_id, { projection: { _id: 1 } }); - - if (!department) { - throw new Meteor.Error('department-not-found', 'Department not found', { - method: 'livechat:removeDepartment', - }); - } - - // TODO: these kind of actions should be on events instead of here - await LivechatDepartmentAgents.enableAgentsByDepartmentId(_id); - return LivechatDepartmentRaw.unarchiveDepartment(_id); - }, - - async archiveDepartment(_id) { - check(_id, String); - - const department = await LivechatDepartmentRaw.findOneById(_id, { projection: { _id: 1 } }); - - if (!department) { - throw new Meteor.Error('department-not-found', 'Department not found', { - method: 'livechat:removeDepartment', - }); - } - - await LivechatDepartmentAgents.disableAgentsByDepartmentId(_id); - await LivechatDepartmentRaw.archiveDepartment(_id); - - this.logger.debug({ msg: 'Running livechat.afterDepartmentArchived callback for department:', departmentId: _id }); - await callbacks.run('livechat.afterDepartmentArchived', department); - }, - showConnecting() { const { showConnecting } = RoutingManager.getConfig(); return showConnecting; @@ -767,28 +732,6 @@ export const Livechat = { }); }, - async getRoomMessages({ rid }) { - check(rid, String); - - const room = await Rooms.findOneById(rid, { projection: { t: 1 } }); - if (room?.t !== 'l') { - throw new Meteor.Error('invalid-room'); - } - - const ignoredMessageTypes = [ - 'livechat_navigation_history', - 'livechat_transcript_history', - 'command', - 'livechat-close', - 'livechat-started', - 'livechat_video_call', - ]; - - return Messages.findVisibleByRoomIdNotContainingTypes(rid, ignoredMessageTypes, { - sort: { ts: 1 }, - }).toArray(); - }, - async requestTranscript({ rid, email, subject, user }) { check(rid, String); check(email, String); @@ -892,20 +835,6 @@ export const Livechat = { }); }, - async notifyAgentStatusChanged(userId, status) { - callbacks.runAsync('livechat.agentStatusChanged', { userId, status }); - if (!settings.get('Livechat_show_agent_info')) { - return; - } - - await LivechatRooms.findOpenByAgent(userId).forEach((room) => { - void api.broadcast('omnichannel.room', room._id, { - type: 'agentStatus', - status, - }); - }); - }, - async allowAgentChangeServiceStatus(statusLivechat, agentId) { if (statusLivechat !== 'available') { return true; @@ -913,56 +842,4 @@ export const Livechat = { return businessHourManager.allowAgentChangeServiceStatus(agentId); }, - - notifyRoomVisitorChange(roomId, visitor) { - void api.broadcast('omnichannel.room', roomId, { - type: 'visitorData', - visitor, - }); - }, - - async changeRoomVisitor(userId, roomId, visitor) { - const user = await Users.findOneById(userId); - if (!user) { - throw new Error('error-user-not-found'); - } - - if (!(await hasPermissionAsync(userId, 'change-livechat-room-visitor'))) { - throw new Error('error-not-authorized'); - } - - const room = await LivechatRooms.findOneById(roomId, { ...roomAccessAttributes, _id: 1, t: 1 }); - - if (!room) { - throw new Meteor.Error('invalid-room'); - } - - if (!(await canAccessRoomAsync(room, user))) { - throw new Error('error-not-allowed'); - } - - await LivechatRooms.changeVisitorByRoomId(room._id, visitor); - - Livechat.notifyRoomVisitorChange(room._id, visitor); - - return LivechatRooms.findOneById(roomId); - }, - async updateLastChat(contactId, lastChat) { - const updateUser = { - $set: { - lastChat, - }, - }; - await LivechatVisitors.updateById(contactId, updateUser); - }, - async updateCallStatus(callId, rid, status, user) { - await Rooms.setCallStatus(rid, status); - if (status === 'ended' || status === 'declined') { - if (await VideoConf.declineLivechatCall(callId)) { - return; - } - - return updateMessage({ _id: callId, msg: status, actionLinks: [], webRtcCallEndTs: new Date() }, user); - } - }, }; diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index c443bc7873c7..afb649488300 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -1,4 +1,4 @@ -import { Message } from '@rocket.chat/core-services'; +import { Message, VideoConf, api } from '@rocket.chat/core-services'; import type { IOmnichannelRoom, IOmnichannelRoomClosingInfo, @@ -23,6 +23,7 @@ import { Users, LivechatDepartmentAgents, ReadReceipts, + Rooms, } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; @@ -32,8 +33,10 @@ import type { FindCursor, UpdateFilter } from 'mongodb'; import { Apps, AppEvents } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; +import { canAccessRoomAsync } from '../../../authorization/server'; import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; +import { updateMessage } from '../../../lib/server/functions/updateMessage'; import * as Mailer from '../../../mailer/server/api'; import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; @@ -808,6 +811,112 @@ class LivechatClass { return true; } + + async updateCallStatus(callId: string, rid: string, status: 'ended' | 'declined', user: IUser | ILivechatVisitor) { + await Rooms.setCallStatus(rid, status); + if (status === 'ended' || status === 'declined') { + if (await VideoConf.declineLivechatCall(callId)) { + return; + } + + return updateMessage({ _id: callId, msg: status, actionLinks: [], webRtcCallEndTs: new Date(), rid }, user as unknown as IUser); + } + } + + async updateLastChat(contactId: string, lastChat: Required) { + const updateUser = { + $set: { + lastChat, + }, + }; + await LivechatVisitors.updateById(contactId, updateUser); + } + + notifyRoomVisitorChange(roomId: string, visitor: ILivechatVisitor) { + void api.broadcast('omnichannel.room', roomId, { + type: 'visitorData', + visitor, + }); + } + + async changeRoomVisitor(userId: string, room: IOmnichannelRoom, visitor: ILivechatVisitor) { + const user = await Users.findOneById(userId, { projection: { _id: 1 } }); + if (!user) { + throw new Error('error-user-not-found'); + } + + if (!(await canAccessRoomAsync(room, user))) { + throw new Error('error-not-allowed'); + } + + await LivechatRooms.changeVisitorByRoomId(room._id, visitor); + + this.notifyRoomVisitorChange(room._id, visitor); + + return LivechatRooms.findOneById(room._id); + } + + async notifyAgentStatusChanged(userId: string, status?: UserStatus) { + if (!status) { + return; + } + + void callbacks.runAsync('livechat.agentStatusChanged', { userId, status }); + if (!settings.get('Livechat_show_agent_info')) { + return; + } + + await LivechatRooms.findOpenByAgent(userId).forEach((room) => { + void api.broadcast('omnichannel.room', room._id, { + type: 'agentStatus', + status, + }); + }); + } + + async getRoomMessages({ rid }: { rid: string }) { + const room = await Rooms.findOneById(rid, { projection: { t: 1 } }); + if (room?.t !== 'l') { + throw new Meteor.Error('invalid-room'); + } + + const ignoredMessageTypes: MessageTypesValues[] = [ + 'livechat_navigation_history', + 'livechat_transcript_history', + 'command', + 'livechat-close', + 'livechat-started', + 'livechat_video_call', + ]; + + return Messages.findVisibleByRoomIdNotContainingTypes(rid, ignoredMessageTypes, { + sort: { ts: 1 }, + }).toArray(); + } + + async archiveDepartment(_id: string) { + const department = await LivechatDepartment.findOneById(_id, { projection: { _id: 1 } }); + + if (!department) { + throw new Error('department-not-found'); + } + + await Promise.all([LivechatDepartmentAgents.disableAgentsByDepartmentId(_id), LivechatDepartment.archiveDepartment(_id)]); + + await callbacks.run('livechat.afterDepartmentArchived', department); + } + + async unarchiveDepartment(_id: string) { + const department = await LivechatDepartment.findOneById(_id, { projection: { _id: 1 } }); + + if (!department) { + throw new Meteor.Error('department-not-found'); + } + + // TODO: these kind of actions should be on events instead of here + await Promise.all([LivechatDepartmentAgents.enableAgentsByDepartmentId(_id), LivechatDepartment.unarchiveDepartment(_id)]); + return true; + } } export const Livechat = new LivechatClass(); diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 169144cc2788..4d59f52e9cd6 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -18,6 +18,7 @@ import type { ILivechatTagRecord, TransferData, AtLeast, + UserStatus, } from '@rocket.chat/core-typings'; import type { FilterOperators } from 'mongodb'; @@ -52,7 +53,7 @@ interface EventLikeCallbackSignatures { 'livechat.saveRoom': (room: IRoom) => void; 'livechat:afterReturnRoomAsInquiry': (params: { room: IRoom }) => void; 'livechat.setUserStatusLivechat': (params: { userId: IUser['_id']; status: OmnichannelAgentStatus }) => void; - 'livechat.agentStatusChanged': (params: { userId: IUser['_id']; status: OmnichannelAgentStatus }) => void; + 'livechat.agentStatusChanged': (params: { userId: IUser['_id']; status: UserStatus }) => void; 'livechat.onNewAgentCreated': (agentId: string) => void; 'livechat.afterTakeInquiry': (inq: InquiryWithAgentInfo, agent: { agentId: string; username: string }) => void; 'livechat.afterAgentRemoved': (params: { agent: Pick }) => void; diff --git a/apps/meteor/server/services/omnichannel/service.ts b/apps/meteor/server/services/omnichannel/service.ts index 7f35de104e1c..61c22505ca98 100644 --- a/apps/meteor/server/services/omnichannel/service.ts +++ b/apps/meteor/server/services/omnichannel/service.ts @@ -2,7 +2,7 @@ import { ServiceClassInternal } from '@rocket.chat/core-services'; import type { IOmnichannelService } from '@rocket.chat/core-services'; import type { IOmnichannelQueue } from '@rocket.chat/core-typings'; -import { Livechat } from '../../../app/livechat/server'; +import { Livechat } from '../../../app/livechat/server/lib/LivechatTyped'; import { RoutingManager } from '../../../app/livechat/server/lib/RoutingManager'; import { settings } from '../../../app/settings/server'; import { OmnichannelQueue } from './queue'; diff --git a/packages/core-services/src/Events.ts b/packages/core-services/src/Events.ts index 88ca1034b9c9..e2a7f624d8df 100644 --- a/packages/core-services/src/Events.ts +++ b/packages/core-services/src/Events.ts @@ -32,6 +32,7 @@ import type { ILivechatInquiryRecord, ILivechatAgent, IBanner, + ILivechatVisitor, } from '@rocket.chat/core-typings'; import type { AutoUpdateRecord } from './types/IMeteor'; @@ -242,7 +243,8 @@ export type EventSignatures = { data: | { type: 'agentStatus'; status: string } | { type: 'queueData'; data: { [k: string]: unknown } | undefined } - | { type: 'agentData'; data: ILivechatAgent | undefined | { hiddenInfo: boolean } }, + | { type: 'agentData'; data: ILivechatAgent | undefined | { hiddenInfo: boolean } } + | { type: 'visitorData'; visitor: ILivechatVisitor }, ): void; // Send all events from here diff --git a/packages/core-typings/src/ILivechatVisitor.ts b/packages/core-typings/src/ILivechatVisitor.ts index e80d63ab15d0..21819cc23f24 100644 --- a/packages/core-typings/src/ILivechatVisitor.ts +++ b/packages/core-typings/src/ILivechatVisitor.ts @@ -7,7 +7,7 @@ export interface IVisitorPhone { export interface IVisitorLastChat { _id: string; - ts: string; + ts: Date; } export interface ILivechatVisitorConnectionData { diff --git a/packages/model-typings/src/models/ILivechatDepartmentModel.ts b/packages/model-typings/src/models/ILivechatDepartmentModel.ts index a074d5c31126..75fe0f54b2eb 100644 --- a/packages/model-typings/src/models/ILivechatDepartmentModel.ts +++ b/packages/model-typings/src/models/ILivechatDepartmentModel.ts @@ -71,4 +71,6 @@ export interface ILivechatDepartmentModel extends IBaseModel; countArchived(): Promise; findEnabledInIds(departmentsIds: string[], options?: FindOptions): FindCursor; + archiveDepartment(_id: string): Promise; + unarchiveDepartment(_id: string): Promise; } From c38711b3464b75905b88ea7eadb289bfb4946061 Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Mon, 16 Oct 2023 15:50:31 -0300 Subject: [PATCH 13/38] feat: Count daily and monthly peaks of concurrent connections (#30573) --- .changeset/perfect-pianos-yawn.md | 5 +++++ .changeset/slow-coats-shout.md | 7 +++++++ .../app/statistics/server/lib/statistics.ts | 7 ++++++- .../admin/info/DeploymentCard.stories.tsx | 2 ++ .../admin/info/InformationPage.stories.tsx | 2 ++ .../views/admin/info/UsageCard.stories.tsx | 2 ++ apps/meteor/server/cron/statistics.ts | 4 +++- apps/meteor/server/models/raw/Statistics.ts | 21 +++++++++++++++++++ ee/packages/presence/src/Presence.ts | 15 +++++++++++++ packages/core-services/src/types/IPresence.ts | 2 ++ packages/core-typings/src/IStats.ts | 2 ++ .../src/models/IStatisticsModel.ts | 1 + 12 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 .changeset/perfect-pianos-yawn.md create mode 100644 .changeset/slow-coats-shout.md diff --git a/.changeset/perfect-pianos-yawn.md b/.changeset/perfect-pianos-yawn.md new file mode 100644 index 000000000000..349bca33ecf7 --- /dev/null +++ b/.changeset/perfect-pianos-yawn.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/presence': minor +--- + +Add peak connections monitoring and methods to get and reset the counter diff --git a/.changeset/slow-coats-shout.md b/.changeset/slow-coats-shout.md new file mode 100644 index 000000000000..4a226e84d161 --- /dev/null +++ b/.changeset/slow-coats-shout.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +--- + +Add the daily and monthly peaks of concurrent connections to statistics + - Added `dailyPeakConnections` statistic for monitoring the daily peak of concurrent connections in a workspace; + - Added `maxMonthlyPeakConnections` statistic for monitoring the last 30 days peak of concurrent connections in a workspace; diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 64543deb88a1..1d60def846b5 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -1,7 +1,7 @@ import { log } from 'console'; import os from 'os'; -import { Analytics, Team, VideoConf } from '@rocket.chat/core-services'; +import { Analytics, Team, VideoConf, Presence } from '@rocket.chat/core-services'; import type { IRoom, IStats } from '@rocket.chat/core-typings'; import { UserStatus } from '@rocket.chat/core-typings'; import { @@ -579,6 +579,11 @@ export const statistics = { const defaultLoggedInCustomScript = (await Settings.findOneById('Custom_Script_Logged_In'))?.packageValue; statistics.loggedInCustomScriptChanged = settings.get('Custom_Script_Logged_In') !== defaultLoggedInCustomScript; + statistics.dailyPeakConnections = await Presence.getPeakConnections(true); + + const peak = await Statistics.findMonthlyPeakConnections(); + statistics.maxMonthlyPeakConnections = Math.max(statistics.dailyPeakConnections, peak?.dailyPeakConnections || 0); + statistics.matrixFederation = await getMatrixFederationStatistics(); // Omnichannel call stats diff --git a/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx b/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx index 41709d247f8b..d3242e68ae2c 100644 --- a/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx +++ b/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx @@ -272,6 +272,8 @@ export default { totalWebRTCCalls: 0, uncaughtExceptionsCount: 0, push: 0, + dailyPeakConnections: 0, + maxMonthlyPeakConnections: 0, matrixFederation: { enabled: false, }, diff --git a/apps/meteor/client/views/admin/info/InformationPage.stories.tsx b/apps/meteor/client/views/admin/info/InformationPage.stories.tsx index 29c0c00d5814..d05c0f7372a2 100644 --- a/apps/meteor/client/views/admin/info/InformationPage.stories.tsx +++ b/apps/meteor/client/views/admin/info/InformationPage.stories.tsx @@ -302,6 +302,8 @@ export default { totalWebRTCCalls: 0, uncaughtExceptionsCount: 0, push: 0, + dailyPeakConnections: 0, + maxMonthlyPeakConnections: 0, matrixFederation: { enabled: false, }, diff --git a/apps/meteor/client/views/admin/info/UsageCard.stories.tsx b/apps/meteor/client/views/admin/info/UsageCard.stories.tsx index a65e645b17d0..4c6cc871ede7 100644 --- a/apps/meteor/client/views/admin/info/UsageCard.stories.tsx +++ b/apps/meteor/client/views/admin/info/UsageCard.stories.tsx @@ -250,6 +250,8 @@ export default { totalWebRTCCalls: 0, uncaughtExceptionsCount: 0, push: 0, + dailyPeakConnections: 0, + maxMonthlyPeakConnections: 0, matrixFederation: { enabled: false, }, diff --git a/apps/meteor/server/cron/statistics.ts b/apps/meteor/server/cron/statistics.ts index 27c1fc064e25..7e7dea6adbc7 100644 --- a/apps/meteor/server/cron/statistics.ts +++ b/apps/meteor/server/cron/statistics.ts @@ -37,5 +37,7 @@ export async function statsCron(logger: Logger): Promise { const now = new Date(); - await cronJobs.add(name, `12 ${now.getHours()} * * *`, async () => generateStatistics(logger)); + await cronJobs.add(name, `12 ${now.getHours()} * * *`, async () => { + await generateStatistics(logger); + }); } diff --git a/apps/meteor/server/models/raw/Statistics.ts b/apps/meteor/server/models/raw/Statistics.ts index 1ad0ab993910..bad44ee07c23 100644 --- a/apps/meteor/server/models/raw/Statistics.ts +++ b/apps/meteor/server/models/raw/Statistics.ts @@ -25,4 +25,25 @@ export class StatisticsRaw extends BaseRaw implements IStatisticsModel { ).toArray(); return records?.[0]; } + + async findMonthlyPeakConnections() { + const oneMonthAgo = new Date(); + oneMonthAgo.setDate(oneMonthAgo.getDate() - 30); + oneMonthAgo.setHours(0, 0, 0, 0); + + return this.findOne>( + { + createdAt: { $gte: oneMonthAgo }, + }, + { + sort: { + dailyPeakConnections: -1, + }, + projection: { + dailyPeakConnections: 1, + createdAt: 1, + }, + }, + ); + } } diff --git a/ee/packages/presence/src/Presence.ts b/ee/packages/presence/src/Presence.ts index fb656fc3e158..5bd69e1f4fc8 100755 --- a/ee/packages/presence/src/Presence.ts +++ b/ee/packages/presence/src/Presence.ts @@ -19,6 +19,8 @@ export class Presence extends ServiceClass implements IPresence { private connsPerInstance = new Map(); + private peakConnections = 0; + constructor() { super(); @@ -35,6 +37,7 @@ export class Presence extends ServiceClass implements IPresence { if (diff?.hasOwnProperty('extraInformation.conns')) { this.connsPerInstance.set(id, diff['extraInformation.conns']); + this.peakConnections = Math.max(this.peakConnections, this.getTotalConnections()); this.validateAvailability(); } }); @@ -251,4 +254,16 @@ export class Presence extends ServiceClass implements IPresence { private getTotalConnections(): number { return Array.from(this.connsPerInstance.values()).reduce((acc, conns) => acc + conns, 0); } + + getPeakConnections(reset = false): number { + const peak = this.peakConnections; + if (reset) { + this.resetPeakConnections(); + } + return peak; + } + + resetPeakConnections(): void { + this.peakConnections = 0; + } } diff --git a/packages/core-services/src/types/IPresence.ts b/packages/core-services/src/types/IPresence.ts index 197f9b685cf8..5f7c57d67995 100644 --- a/packages/core-services/src/types/IPresence.ts +++ b/packages/core-services/src/types/IPresence.ts @@ -19,4 +19,6 @@ export interface IPresence extends IServiceClass { updateUserPresence(uid: string): Promise; toggleBroadcast(enabled: boolean): void; getConnectionCount(): { current: number; max: number }; + getPeakConnections(reset?: boolean): number; + resetPeakConnections(): void; } diff --git a/packages/core-typings/src/IStats.ts b/packages/core-typings/src/IStats.ts index 443cbfb23957..7fd5cd8218bc 100644 --- a/packages/core-typings/src/IStats.ts +++ b/packages/core-typings/src/IStats.ts @@ -219,6 +219,8 @@ export interface IStats { totalWebRTCCalls: number; uncaughtExceptionsCount: number; push: number; + dailyPeakConnections: number; + maxMonthlyPeakConnections: number; matrixFederation: { enabled: boolean; }; diff --git a/packages/model-typings/src/models/IStatisticsModel.ts b/packages/model-typings/src/models/IStatisticsModel.ts index ac84a49525b3..fe4534eaee0f 100644 --- a/packages/model-typings/src/models/IStatisticsModel.ts +++ b/packages/model-typings/src/models/IStatisticsModel.ts @@ -4,4 +4,5 @@ import type { IBaseModel } from './IBaseModel'; export interface IStatisticsModel extends IBaseModel { findLast(): Promise; + findMonthlyPeakConnections(): Promise | null>; } From 809eb63d79d03cbbde4d40f4970f5cc25ffe4f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=87=E3=83=AF=E3=83=B3=E3=82=B7=E3=83=A5?= <61188295+Dnouv@users.noreply.github.com> Date: Tue, 17 Oct 2023 01:20:26 +0530 Subject: [PATCH 14/38] fix: apps typing indicator not working (#30360) --- .changeset/rich-dogs-smell.md | 5 +++++ apps/meteor/app/apps/server/bridges/messages.ts | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .changeset/rich-dogs-smell.md diff --git a/.changeset/rich-dogs-smell.md b/.changeset/rich-dogs-smell.md new file mode 100644 index 000000000000..be27db28e227 --- /dev/null +++ b/.changeset/rich-dogs-smell.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Fix typing indicator of Apps user diff --git a/apps/meteor/app/apps/server/bridges/messages.ts b/apps/meteor/app/apps/server/bridges/messages.ts index d75a0c244674..e4d09018176d 100644 --- a/apps/meteor/app/apps/server/bridges/messages.ts +++ b/apps/meteor/app/apps/server/bridges/messages.ts @@ -103,7 +103,11 @@ export class AppMessageBridge extends MessageBridge { protected async typing({ scope, id, username, isTyping }: ITypingDescriptor): Promise { switch (scope) { case 'room': - notifications.notifyRoom(id, 'typing', username!, isTyping); + if (!username) { + throw new Error('Invalid username'); + } + + notifications.notifyRoom(id, 'user-activity', username, isTyping ? ['user-typing'] : []); return; default: throw new Error('Unrecognized typing scope provided'); From a0dcc38574c10a250b266209a783bea48a13fddd Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Mon, 16 Oct 2023 17:35:46 -0300 Subject: [PATCH 15/38] regression: failing to create rooms in some integrations (#30649) --- apps/meteor/app/cas/server/cas_server.js | 2 +- .../app/irc/server/irc-bridge/peerHandlers/joinedChannel.js | 2 +- apps/meteor/app/slackbridge/server/RocketAdapter.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/app/cas/server/cas_server.js b/apps/meteor/app/cas/server/cas_server.js index 25d3b9fdd698..60880c77d4f4 100644 --- a/apps/meteor/app/cas/server/cas_server.js +++ b/apps/meteor/app/cas/server/cas_server.js @@ -257,7 +257,7 @@ Accounts.registerLoginHandler('cas', async (options) => { if (roomName) { let room = await Rooms.findOneByNameAndType(roomName, 'c'); if (!room) { - room = await createRoom('c', roomName, user.username); + room = await createRoom('c', roomName, user); } } } diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.js index 0968eacc5340..bb5053ffdd71 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.js @@ -16,7 +16,7 @@ export default async function handleJoinedChannel(args) { let room = await Rooms.findOneByName(args.roomName); if (!room) { - const createdRoom = await createRoom('c', args.roomName, user.username, []); + const createdRoom = await createRoom('c', args.roomName, user, []); room = await Rooms.findOne({ _id: createdRoom.rid }); this.log(`${user.username} created room ${args.roomName}`); diff --git a/apps/meteor/app/slackbridge/server/RocketAdapter.js b/apps/meteor/app/slackbridge/server/RocketAdapter.js index d0ef8157137d..f76c33fa1f81 100644 --- a/apps/meteor/app/slackbridge/server/RocketAdapter.js +++ b/apps/meteor/app/slackbridge/server/RocketAdapter.js @@ -295,7 +295,7 @@ export default class RocketAdapter { try { const isPrivate = slackChannel.is_private; - const rocketChannel = await createRoom(isPrivate ? 'p' : 'c', slackChannel.name, rocketUserCreator.username, rocketUsers); + const rocketChannel = await createRoom(isPrivate ? 'p' : 'c', slackChannel.name, rocketUserCreator, rocketUsers); slackChannel.rocketId = rocketChannel.rid; } catch (e) { if (!hasRetried) { From 85ddfb24baccdcbae56ffaf7a070b83128b4c7fb Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Mon, 16 Oct 2023 18:46:51 -0300 Subject: [PATCH 16/38] fix: licenses.info endpoint only available for admins (#30644) --- apps/meteor/ee/server/api/licenses.ts | 7 ++- .../tests/end-to-end/api/20-licenses.js | 46 +++++++++++++++++++ .../license/src/definition/LicenseInfo.ts | 10 ++++ ee/packages/license/src/index.ts | 11 ++--- ee/packages/license/src/license.ts | 19 +++++--- packages/rest-typings/src/v1/licenses.ts | 8 +--- 6 files changed, 80 insertions(+), 21 deletions(-) create mode 100644 ee/packages/license/src/definition/LicenseInfo.ts diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index ff5c3fcc3e47..b7ac3ba81e9c 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -25,10 +25,13 @@ API.v1.addRoute( API.v1.addRoute( 'licenses.info', - { authRequired: true, validateParams: isLicensesInfoProps, permissionsRequired: ['view-privileged-setting'] }, + { authRequired: true, validateParams: isLicensesInfoProps }, { async get() { - const data = await License.getInfo(Boolean(this.queryParams.loadValues)); + const unrestrictedAccess = await hasPermissionAsync(this.userId, 'view-privileged-setting'); + const loadCurrentValues = unrestrictedAccess && Boolean(this.queryParams.loadValues); + + const data = await License.getInfo({ limits: unrestrictedAccess, license: unrestrictedAccess, currentValues: loadCurrentValues }); return API.v1.success({ data }); }, diff --git a/apps/meteor/tests/end-to-end/api/20-licenses.js b/apps/meteor/tests/end-to-end/api/20-licenses.js index 993428d34409..302011addef9 100644 --- a/apps/meteor/tests/end-to-end/api/20-licenses.js +++ b/apps/meteor/tests/end-to-end/api/20-licenses.js @@ -105,6 +105,52 @@ describe('licenses', function () { }); }); + describe('[/licenses.info]', () => { + it('should fail if not logged in', (done) => { + request + .get(api('licenses.info')) + .expect('Content-Type', 'application/json') + .expect(401) + .expect((res) => { + expect(res.body).to.have.property('status', 'error'); + expect(res.body).to.have.property('message'); + }) + .end(done); + }); + + it('should return limited information if user is unauthorized', (done) => { + request + .get(api('licenses.info')) + .set(unauthorizedUserCredentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('data').and.to.be.an('object'); + expect(res.body.data).to.not.have.property('license'); + expect(res.body.data).to.have.property('tags').and.to.be.an('array'); + }) + .end(done); + }); + + it('should return unrestricted info if user is logged in and is authorized', (done) => { + request + .get(api('licenses.info')) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('data').and.to.be.an('object'); + if (process.env.IS_EE) { + expect(res.body.data).to.have.property('license').and.to.be.an('object'); + } + expect(res.body.data).to.have.property('tags').and.to.be.an('array'); + }) + + .end(done); + }); + }); + describe('[/licenses.isEnterprise]', () => { it('should fail if not logged in', (done) => { request diff --git a/ee/packages/license/src/definition/LicenseInfo.ts b/ee/packages/license/src/definition/LicenseInfo.ts new file mode 100644 index 000000000000..7de3c0cfbdd6 --- /dev/null +++ b/ee/packages/license/src/definition/LicenseInfo.ts @@ -0,0 +1,10 @@ +import type { ILicenseTag } from './ILicenseTag'; +import type { ILicenseV3, LicenseLimitKind } from './ILicenseV3'; +import type { LicenseModule } from './LicenseModule'; + +export type LicenseInfo = { + license?: ILicenseV3; + activeModules: LicenseModule[]; + limits: Record; + tags: ILicenseTag[]; +}; diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts index 77e2976f156a..9707a41d96ab 100644 --- a/ee/packages/license/src/index.ts +++ b/ee/packages/license/src/index.ts @@ -1,5 +1,5 @@ -import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; -import type { LicenseModule } from './definition/LicenseModule'; +import type { LicenseLimitKind } from './definition/ILicenseV3'; +import type { LicenseInfo } from './definition/LicenseInfo'; import type { LimitContext } from './definition/LimitContext'; import { getAppsConfig, getMaxActiveUsers, getUnmodifiedLicenseAndModules } from './deprecated'; import { onLicense } from './events/deprecated'; @@ -24,6 +24,7 @@ export * from './definition/ILicenseTag'; export * from './definition/ILicenseV2'; export * from './definition/ILicenseV3'; export * from './definition/LicenseBehavior'; +export * from './definition/LicenseInfo'; export * from './definition/LicenseLimit'; export * from './definition/LicenseModule'; export * from './definition/LicensePeriod'; @@ -49,11 +50,7 @@ interface License { onBehaviorTriggered: typeof onBehaviorTriggered; revalidateLicense: () => Promise; - getInfo: (loadCurrentValues: boolean) => Promise<{ - license: ILicenseV3 | undefined; - activeModules: LicenseModule[]; - limits: Record; - }>; + getInfo: (info: { limits: boolean; currentValues: boolean; license: boolean }) => Promise; // Deprecated: onLicense: typeof onLicense; diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 8449d4136810..d24d91287d1e 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -4,6 +4,7 @@ import { type ILicenseTag } from './definition/ILicenseTag'; import type { ILicenseV2 } from './definition/ILicenseV2'; import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; import type { BehaviorWithContext } from './definition/LicenseBehavior'; +import type { LicenseInfo } from './definition/LicenseInfo'; import type { LicenseModule } from './definition/LicenseModule'; import type { LicenseValidationOptions } from './definition/LicenseValidationOptions'; import type { LimitContext } from './definition/LimitContext'; @@ -291,17 +292,22 @@ export class LicenseManager extends Emitter { return isBehaviorsInResult(validationResult, ['prevent_action']); } - public async getInfo(loadCurrentValues = false): Promise<{ - license: ILicenseV3 | undefined; - activeModules: LicenseModule[]; - limits: Record; - }> { + public async getInfo({ + limits: includeLimits, + currentValues: loadCurrentValues, + license: includeLicense, + }: { + limits: boolean; + currentValues: boolean; + license: boolean; + }): Promise { const activeModules = getModules.call(this); const license = this.getLicense(); // Get all limits present in the license and their current value const limits = ( (license && + includeLimits && (await Promise.all( globalLimitKinds .map((limitKey) => ({ @@ -322,9 +328,10 @@ export class LicenseManager extends Emitter { ).reduce((prev, curr) => ({ ...prev, ...curr }), {}); return { - license, + license: (includeLicense && license) || undefined, activeModules, limits: limits as Record, + tags: license?.information.tags || [], }; } } diff --git a/packages/rest-typings/src/v1/licenses.ts b/packages/rest-typings/src/v1/licenses.ts index 87c0106f6d3f..d229ca49f1fc 100644 --- a/packages/rest-typings/src/v1/licenses.ts +++ b/packages/rest-typings/src/v1/licenses.ts @@ -1,4 +1,4 @@ -import type { ILicenseV2, ILicenseV3, LicenseLimitKind } from '@rocket.chat/license'; +import type { ILicenseV2, ILicenseV3, LicenseInfo } from '@rocket.chat/license'; import Ajv from 'ajv'; const ajv = new Ajv({ @@ -45,11 +45,7 @@ export type LicensesEndpoints = { }; '/v1/licenses.info': { GET: (params: licensesInfoProps) => { - data: { - license: ILicenseV3 | undefined; - activeModules: string[]; - limits: Record; - }; + data: LicenseInfo; }; }; '/v1/licenses.add': { From ff2263a3c11d59f9e964c4f1f6b6926521f9283c Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:25:51 -0300 Subject: [PATCH 17/38] fix: Read receipts are not created on the first time a user reads a room (#30610) Co-authored-by: Heitor Tanoue <68477006+heitortanoue@users.noreply.github.com> --- .changeset/weak-cameras-pay.md | 5 +++++ apps/meteor/server/lib/readMessages.ts | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .changeset/weak-cameras-pay.md diff --git a/.changeset/weak-cameras-pay.md b/.changeset/weak-cameras-pay.md new file mode 100644 index 000000000000..724f3af69a29 --- /dev/null +++ b/.changeset/weak-cameras-pay.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed issue with message read receipts not being created when accessing a room the first time diff --git a/apps/meteor/server/lib/readMessages.ts b/apps/meteor/server/lib/readMessages.ts index 00bf04bd3449..d7c8cf559288 100644 --- a/apps/meteor/server/lib/readMessages.ts +++ b/apps/meteor/server/lib/readMessages.ts @@ -6,7 +6,7 @@ import { callbacks } from '../../lib/callbacks'; export async function readMessages(rid: IRoom['_id'], uid: IUser['_id'], readThreads: boolean): Promise { await callbacks.run('beforeReadMessages', rid, uid); - const projection = { ls: 1, tunread: 1, alert: 1 }; + const projection = { ls: 1, tunread: 1, alert: 1, ts: 1 }; const sub = await Subscriptions.findOneByRoomIdAndUserId(rid, uid, { projection }); if (!sub) { throw new Error('error-invalid-subscription'); @@ -19,5 +19,6 @@ export async function readMessages(rid: IRoom['_id'], uid: IUser['_id'], readThr await NotificationQueue.clearQueueByUserId(uid); - callbacks.runAsync('afterReadMessages', rid, { uid, lastSeen: sub.ls }); + const lastSeen = sub.ls || sub.ts; + callbacks.runAsync('afterReadMessages', rid, { uid, lastSeen }); } From dd5b236895f754bbec857fe9c0d4f17ecaa28465 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Tue, 17 Oct 2023 12:07:28 -0300 Subject: [PATCH 18/38] chore: remove license v3 public key envvar (#30646) --- ee/packages/license/babel.config.json | 11 ----- ee/packages/license/package.json | 9 +--- ee/packages/license/src/token.ts | 2 +- yarn.lock | 65 +++------------------------ 4 files changed, 7 insertions(+), 80 deletions(-) delete mode 100644 ee/packages/license/babel.config.json diff --git a/ee/packages/license/babel.config.json b/ee/packages/license/babel.config.json deleted file mode 100644 index e154c0813530..000000000000 --- a/ee/packages/license/babel.config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "presets": ["@babel/preset-typescript"], - "plugins": [ - [ - "transform-inline-environment-variables", - { - "include": ["LICENSE_PUBLIC_KEY_V3"] - } - ] - ] -} diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 6810f53e40dd..ec79532a9680 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -3,18 +3,11 @@ "version": "0.0.1", "private": true, "devDependencies": { - "@babel/cli": "^7.23.0", - "@babel/core": "^7.23.0", - "@babel/preset-env": "^7.22.20", - "@babel/preset-typescript": "^7.23.0", "@swc/core": "^1.3.66", "@swc/jest": "^0.2.26", - "@types/babel__core": "^7", - "@types/babel__preset-env": "^7", "@types/bcrypt": "^5.0.0", "@types/jest": "~29.5.3", "@types/ws": "^8.5.5", - "babel-plugin-transform-inline-environment-variables": "^0.4.4", "eslint": "~8.45.0", "jest": "~29.6.1", "jest-environment-jsdom": "~29.6.1", @@ -29,7 +22,7 @@ "testunit": "jest", "build": "npm run build:types && npm run build:js", "build:types": "tsc --emitDeclarationOnly", - "build:js": "babel src --out-dir dist --extensions \".ts,.tsx\" --source-maps inline", + "build:js": "rm -rf dist && tsc -p tsconfig.json", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" }, "main": "./dist/index.js", diff --git a/ee/packages/license/src/token.ts b/ee/packages/license/src/token.ts index 2a9836a48303..46daaef83974 100644 --- a/ee/packages/license/src/token.ts +++ b/ee/packages/license/src/token.ts @@ -7,7 +7,7 @@ import type { ILicenseV3 } from './definition/ILicenseV3'; const PUBLIC_LICENSE_KEY_V2 = 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUFxV1Nza2Q5LzZ6Ung4a3lQY2ljcwpiMzJ3Mnd4VnV3N3lCVDk2clEvOEQreU1lQ01POXdTU3BIYS85bkZ5d293RXRpZ3B0L3dyb1BOK1ZHU3didHdQCkZYQmVxRWxCbmRHRkFsODZlNStFbGlIOEt6L2hHbkNtSk5tWHB4RUsyUkUwM1g0SXhzWVg3RERCN010eC9pcXMKY2pCL091dlNCa2ppU2xlUzdibE5JVC9kQTdLNC9DSjNvaXUwMmJMNEV4Y2xDSGVwenFOTWVQM3dVWmdweE9uZgpOT3VkOElYWUs3M3pTY3VFOEUxNTdZd3B6Q0twVmFIWDdaSmY4UXVOc09PNVcvYUlqS2wzTDYyNjkrZUlPRXJHCndPTm1hSG56Zmc5RkxwSmh6Z3BPMzhhVm43NnZENUtLakJhaldza1krNGEyZ1NRbUtOZUZxYXFPb3p5RUZNMGUKY0ZXWlZWWjNMZWg0dkVNb1lWUHlJeng5Nng4ZjIveW1QbmhJdXZRdjV3TjRmeWVwYTdFWTVVQ2NwNzF6OGtmUAo0RmNVelBBMElEV3lNaWhYUi9HNlhnUVFaNEdiL3FCQmh2cnZpSkNGemZZRGNKZ0w3RmVnRllIUDNQR0wwN1FnCnZMZXZNSytpUVpQcnhyYnh5U3FkUE9rZ3VyS2pWclhUVXI0QTlUZ2lMeUlYNVVsSnEzRS9SVjdtZk9xWm5MVGEKU0NWWEhCaHVQbG5DR1pSMDFUb1RDZktoTUcxdTBDRm5MMisxNWhDOWZxT21XdjlRa2U0M3FsSjBQZ0YzVkovWAp1eC9tVHBuazlnbmJHOUpIK21mSDM5Um9GdlROaW5Zd1NNdll6dXRWT242OXNPemR3aERsYTkwbDNBQ2g0eENWCks3Sk9YK3VIa29OdTNnMmlWeGlaVU0wQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo='; -const PUBLIC_LICENSE_KEY_V3 = process.env.PUBLIC_LICENSE_KEY_V3 || PUBLIC_LICENSE_KEY_V2; +const PUBLIC_LICENSE_KEY_V3 = PUBLIC_LICENSE_KEY_V2; let TEST_KEYS: [string, string] | undefined = undefined; diff --git a/yarn.lock b/yarn.lock index b4e4af200f30..ce6dc859fc3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -966,33 +966,6 @@ __metadata: languageName: node linkType: hard -"@babel/cli@npm:^7.23.0": - version: 7.23.0 - resolution: "@babel/cli@npm:7.23.0" - dependencies: - "@jridgewell/trace-mapping": ^0.3.17 - "@nicolo-ribaudo/chokidar-2": 2.1.8-no-fsevents.3 - chokidar: ^3.4.0 - commander: ^4.0.1 - convert-source-map: ^2.0.0 - fs-readdir-recursive: ^1.1.0 - glob: ^7.2.0 - make-dir: ^2.1.0 - slash: ^2.0.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - dependenciesMeta: - "@nicolo-ribaudo/chokidar-2": - optional: true - chokidar: - optional: true - bin: - babel: ./bin/babel.js - babel-external-helpers: ./bin/babel-external-helpers.js - checksum: beeb189560bf9c4ea951ef637eefa5214654678fb09c4aaa6695921037059c1e1553c610fe95fbd19a9cdfd9f5598a812fc13df40a6b9a9ea899e43fc6c42052 - languageName: node - linkType: hard - "@babel/code-frame@npm:7.12.11": version: 7.12.11 resolution: "@babel/code-frame@npm:7.12.11" @@ -1043,7 +1016,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.1.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.10, @babel/core@npm:^7.12.3, @babel/core@npm:^7.20.7, @babel/core@npm:^7.21.4, @babel/core@npm:^7.23.0, @babel/core@npm:^7.7.5": +"@babel/core@npm:^7.1.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.10, @babel/core@npm:^7.12.3, @babel/core@npm:^7.20.7, @babel/core@npm:^7.21.4, @babel/core@npm:^7.7.5": version: 7.23.0 resolution: "@babel/core@npm:7.23.0" dependencies: @@ -2513,7 +2486,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:^7.12.11, @babel/preset-env@npm:^7.22.20, @babel/preset-env@npm:~7.22.10, @babel/preset-env@npm:~7.22.9": +"@babel/preset-env@npm:^7.12.11, @babel/preset-env@npm:~7.22.10, @babel/preset-env@npm:~7.22.9": version: 7.22.20 resolution: "@babel/preset-env@npm:7.22.20" dependencies: @@ -2645,7 +2618,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-typescript@npm:^7.12.7, @babel/preset-typescript@npm:^7.23.0": +"@babel/preset-typescript@npm:^7.12.7": version: 7.23.0 resolution: "@babel/preset-typescript@npm:7.23.0" dependencies: @@ -4588,13 +4561,6 @@ __metadata: languageName: node linkType: hard -"@nicolo-ribaudo/chokidar-2@npm:2.1.8-no-fsevents.3": - version: 2.1.8-no-fsevents.3 - resolution: "@nicolo-ribaudo/chokidar-2@npm:2.1.8-no-fsevents.3" - checksum: ee55cc9241aeea7eb94b8a8551bfa4246c56c53bc71ecda0a2104018fcc328ba5723b33686bdf9cc65d4df4ae65e8016b89e0bbdeb94e0309fe91bb9ced42344 - languageName: node - linkType: hard - "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1": version: 5.1.1-v1 resolution: "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1" @@ -8474,21 +8440,14 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/license@workspace:ee/packages/license" dependencies: - "@babel/cli": ^7.23.0 - "@babel/core": ^7.23.0 - "@babel/preset-env": ^7.22.20 - "@babel/preset-typescript": ^7.23.0 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/jwt": "workspace:^" "@rocket.chat/logger": "workspace:^" "@swc/core": ^1.3.66 "@swc/jest": ^0.2.26 - "@types/babel__core": ^7 - "@types/babel__preset-env": ^7 "@types/bcrypt": ^5.0.0 "@types/jest": ~29.5.3 "@types/ws": ^8.5.5 - babel-plugin-transform-inline-environment-variables: ^0.4.4 bcrypt: ^5.0.1 eslint: ~8.45.0 jest: ~29.6.1 @@ -15433,13 +15392,6 @@ __metadata: languageName: node linkType: hard -"babel-plugin-transform-inline-environment-variables@npm:^0.4.4": - version: 0.4.4 - resolution: "babel-plugin-transform-inline-environment-variables@npm:0.4.4" - checksum: fa361287411301237fd8ce332aff4f8e8ccb8db30e87a2ddc7224c8bf7cd792eda47aca24dc2e09e70bce4c027bc8cbe22f4999056be37a25d2472945df21ef5 - languageName: node - linkType: hard - "babel-polyfill@npm:^6.2.0": version: 6.26.0 resolution: "babel-polyfill@npm:6.26.0" @@ -16964,7 +16916,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:3.5.3, chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.4.0, chokidar@npm:^3.4.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": +"chokidar@npm:3.5.3, chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.4.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": version: 3.5.3 resolution: "chokidar@npm:3.5.3" dependencies: @@ -17513,7 +17465,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^4.0.0, commander@npm:^4.0.1, commander@npm:^4.1.1": +"commander@npm:^4.0.0, commander@npm:^4.1.1": version: 4.1.1 resolution: "commander@npm:4.1.1" checksum: d7b9913ff92cae20cb577a4ac6fcc121bd6223319e54a40f51a14740a681ad5c574fd29a57da478a5f234a6fa6c52cbf0b7c641353e03c648b1ae85ba670b977 @@ -22120,13 +22072,6 @@ __metadata: languageName: node linkType: hard -"fs-readdir-recursive@npm:^1.1.0": - version: 1.1.0 - resolution: "fs-readdir-recursive@npm:1.1.0" - checksum: 29d50f3d2128391c7fc9fd051c8b7ea45bcc8aa84daf31ef52b17218e20bfd2bd34d02382742801954cc8d1905832b68227f6b680a666ce525d8b6b75068ad1e - languageName: node - linkType: hard - "fs-write-stream-atomic@npm:^1.0.8": version: 1.0.10 resolution: "fs-write-stream-atomic@npm:1.0.10" From d6fa895e84007a898e410691292f4647dcc3acb6 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 17 Oct 2023 13:55:56 -0600 Subject: [PATCH 19/38] refactor: Move functions out of `Livechat.js` (#30650) --- .../app/apps/server/bridges/livechat.ts | 5 +- .../app/livechat/server/api/lib/livechat.ts | 3 +- .../app/livechat/server/api/v1/message.ts | 4 +- .../livechat/server/api/v1/offlineMessage.ts | 2 +- .../meteor/app/livechat/server/api/v1/room.ts | 14 +- apps/meteor/app/livechat/server/lib/Helper.ts | 3 + .../app/livechat/server/lib/Livechat.js | 213 ------------------ .../app/livechat/server/lib/LivechatTyped.ts | 208 ++++++++++++++++- .../livechat/server/lib/stream/agentStatus.ts | 2 +- .../server/methods/sendOfflineMessage.ts | 2 +- .../server/methods/setDepartmentForVisitor.ts | 2 +- .../app/livechat/server/methods/transfer.ts | 6 +- .../core-typings/src/omnichannel/routing.ts | 4 +- 13 files changed, 236 insertions(+), 232 deletions(-) diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 0ace08bb8446..5b6c76257667 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -74,7 +74,8 @@ export class AppLivechatBridge extends LivechatBridge { message: await this.orch.getConverters()?.get('messages').convertAppMessage(message), }; - await Livechat.updateMessage(data); + // @ts-expect-error IVisitor vs ILivechatVisitor :( + await LivechatTyped.updateMessage(data); } protected async createRoom(visitor: IVisitor, agent: IUser, appId: string, extraParams?: IExtraRoomParams): Promise { @@ -208,7 +209,7 @@ export class AppLivechatBridge extends LivechatBridge { userId = transferredTo._id; } - return Livechat.transfer( + return LivechatTyped.transfer( await this.orch.getConverters()?.get('rooms').convertAppRoom(currentRoom), this.orch.getConverters()?.get('visitors').convertAppVisitor(visitor), { userId, departmentId, transferredBy, transferredTo }, diff --git a/apps/meteor/app/livechat/server/api/lib/livechat.ts b/apps/meteor/app/livechat/server/api/lib/livechat.ts index 7bb608090557..2b72065345d6 100644 --- a/apps/meteor/app/livechat/server/api/lib/livechat.ts +++ b/apps/meteor/app/livechat/server/api/lib/livechat.ts @@ -13,7 +13,6 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../../lib/callbacks'; import { i18n } from '../../../../../server/lib/i18n'; import { normalizeAgent } from '../../lib/Helper'; -import { Livechat } from '../../lib/Livechat'; import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; export function online(department: string, skipSettingCheck = false, skipFallbackCheck = false): Promise { @@ -139,7 +138,7 @@ export function normalizeHttpHeaderData(headers: Record> { // Putting this ugly conversion while we type the livechat service - const initSettings = (await Livechat.getInitSettings()) as unknown as Record; + const initSettings = await LivechatTyped.getInitSettings(); const triggers = await findTriggers(); const departments = await findDepartments(businessUnit); const sound = `${Meteor.absoluteUrl()}sounds/chime.mp3`; diff --git a/apps/meteor/app/livechat/server/api/v1/message.ts b/apps/meteor/app/livechat/server/api/v1/message.ts index 104e2ece94d5..0d5a22b90d89 100644 --- a/apps/meteor/app/livechat/server/api/v1/message.ts +++ b/apps/meteor/app/livechat/server/api/v1/message.ts @@ -134,9 +134,9 @@ API.v1.addRoute( throw new Error('invalid-message'); } - const result = await Livechat.updateMessage({ + const result = await LivechatTyped.updateMessage({ guest, - message: { _id: msg._id, msg: this.bodyParams.msg }, + message: { _id: msg._id, msg: this.bodyParams.msg, rid: msg.rid }, }); if (!result) { return API.v1.failure(); diff --git a/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts b/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts index b01e60d2265f..6acd6ab98ea1 100644 --- a/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts +++ b/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts @@ -2,7 +2,7 @@ import { isPOSTLivechatOfflineMessageParams } from '@rocket.chat/rest-typings'; import { i18n } from '../../../../../server/lib/i18n'; import { API } from '../../../../api/server'; -import { Livechat } from '../../lib/Livechat'; +import { Livechat } from '../../lib/LivechatTyped'; API.v1.addRoute( 'livechat/offline.message', diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 8f6151797463..4f3b4eb6234d 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -251,7 +251,7 @@ API.v1.addRoute( const { _id, username, name } = guest; const transferredBy = normalizeTransferredByData({ _id, username, name, userType: 'visitor' }, room); - if (!(await Livechat.transfer(room, guest, { roomId: rid, departmentId: department, transferredBy }))) { + if (!(await LivechatTyped.transfer(room, guest, { departmentId: department, transferredBy }))) { return API.v1.failure(); } @@ -312,10 +312,10 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-l-room', 'transfer-livechat-guest'], validateParams: isLiveChatRoomForwardProps }, { async post() { - const transferData: typeof this.bodyParams & { - transferredBy?: unknown; + const transferData = this.bodyParams as typeof this.bodyParams & { + transferredBy: TransferByData; transferredTo?: { _id: string; username?: string; name?: string }; - } = this.bodyParams; + }; const room = await LivechatRooms.findOneById(this.bodyParams.roomId); if (!room || room.t !== 'l') { @@ -327,6 +327,10 @@ API.v1.addRoute( } const guest = await LivechatVisitors.findOneEnabledById(room.v?._id); + if (!guest) { + throw new Error('error-invalid-visitor'); + } + const transferedBy = this.user satisfies TransferByData; transferData.transferredBy = normalizeTransferredByData(transferedBy, room); if (transferData.userId) { @@ -340,7 +344,7 @@ API.v1.addRoute( } } - const chatForwardedResult = await Livechat.transfer(room, guest, transferData); + const chatForwardedResult = await LivechatTyped.transfer(room, guest, transferData); if (!chatForwardedResult) { throw new Error('error-forwarding-chat'); } diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index 75722e709b17..63cbbd6998ef 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -402,6 +402,9 @@ export const forwardRoomToAgent = async (room: IOmnichannelRoom, transferData: T logger.debug(`Forwarding room ${room._id} to agent ${transferData.userId}`); const { userId: agentId, clientAction } = transferData; + if (!agentId) { + throw new Error('error-invalid-agent'); + } const user = await Users.findOneOnlineAgentById(agentId); if (!user) { logger.debug(`Agent ${agentId} is offline. Cannot forward`); diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index e1d6626c7ddb..837a8eb7309b 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -1,21 +1,15 @@ // Note: Please don't add any new methods to this file, since its still in js and we are migrating to ts // Please add new methods to LivechatTyped.ts - -import dns from 'dns'; -import util from 'util'; - import { Message } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; import { LivechatVisitors, LivechatCustomField, - Settings, LivechatRooms, LivechatInquiry, Subscriptions, Messages, LivechatDepartment as LivechatDepartmentRaw, - LivechatDepartmentAgents, Rooms, Users, ReadReceipts, @@ -34,7 +28,6 @@ import { hasPermissionAsync } from '../../../authorization/server/functions/hasP import { FileUpload } from '../../../file-upload/server'; import { deleteMessage } from '../../../lib/server/functions/deleteMessage'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; -import { updateMessage } from '../../../lib/server/functions/updateMessage'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; import { businessHourManager } from '../business-hour'; @@ -45,8 +38,6 @@ import { RoutingManager } from './RoutingManager'; const logger = new Logger('Livechat'); -const dnsResolveMx = util.promisify(dns.resolveMx); - export const Livechat = { Analytics, @@ -63,28 +54,6 @@ export const Livechat = { }); }, - async updateMessage({ guest, message }) { - check(message, Match.ObjectIncluding({ _id: String })); - - const originalMessage = await Messages.findOneById(message._id); - if (!originalMessage || !originalMessage._id) { - return; - } - - const editAllowed = settings.get('Message_AllowEditing'); - const editOwn = originalMessage.u && originalMessage.u._id === guest._id; - - if (!editAllowed || !editOwn) { - throw new Meteor.Error('error-action-not-allowed', 'Message editing not allowed', { - method: 'livechatUpdateMessage', - }); - } - - await updateMessage(message, guest); - - return true; - }, - async deleteMessage({ guest, message }) { Livechat.logger.debug(`Attempting to delete a message by visitor ${guest._id}`); check(message, Match.ObjectIncluding({ _id: String })); @@ -188,50 +157,6 @@ export const Livechat = { return 0; }, - async getInitSettings() { - const rcSettings = {}; - - await Settings.findNotHiddenPublic([ - 'Livechat_title', - 'Livechat_title_color', - 'Livechat_enable_message_character_limit', - 'Livechat_message_character_limit', - 'Message_MaxAllowedSize', - 'Livechat_enabled', - 'Livechat_registration_form', - 'Livechat_allow_switching_departments', - 'Livechat_offline_title', - 'Livechat_offline_title_color', - 'Livechat_offline_message', - 'Livechat_offline_success_message', - 'Livechat_offline_form_unavailable', - 'Livechat_display_offline_form', - 'Omnichannel_call_provider', - 'Language', - 'Livechat_enable_transcript', - 'Livechat_transcript_message', - 'Livechat_fileupload_enabled', - 'FileUpload_Enabled', - 'Livechat_conversation_finished_message', - 'Livechat_conversation_finished_text', - 'Livechat_name_field_registration_form', - 'Livechat_email_field_registration_form', - 'Livechat_registration_form_message', - 'Livechat_force_accept_data_processing_consent', - 'Livechat_data_processing_consent_text', - 'Livechat_show_agent_info', - 'Livechat_clear_local_storage_when_chat_ended', - ]).forEach((setting) => { - rcSettings[setting._id] = setting.value; - }); - - rcSettings.Livechat_history_monitor_type = settings.get('Livechat_history_monitor_type'); - - rcSettings.Livechat_Show_Connecting = this.showConnecting(); - - return rcSettings; - }, - async saveRoomInfo(roomData, guestData, userId) { Livechat.logger.debug(`Saving room information on room ${roomData._id}`); const { livechatData = {} } = roomData; @@ -280,35 +205,6 @@ export const Livechat = { } }, - async closeOpenChats(userId, comment) { - Livechat.logger.debug(`Closing open chats for user ${userId}`); - const user = await Users.findOneById(userId); - - const extraQuery = await callbacks.run('livechat.applyDepartmentRestrictions', {}, { userId }); - const openChats = LivechatRooms.findOpenByAgent(userId, extraQuery); - const promises = []; - await openChats.forEach((room) => { - promises.push(LivechatTyped.closeRoom({ user, room, comment })); - }); - - await Promise.all(promises); - }, - - async forwardOpenChats(userId) { - Livechat.logger.debug(`Transferring open chats for user ${userId}`); - for await (const room of LivechatRooms.findOpenByAgent(userId)) { - const guest = await LivechatVisitors.findOneEnabledById(room.v._id); - const user = await Users.findOneById(userId); - const { _id, username, name } = user; - const transferredBy = normalizeTransferredByData({ _id, username, name }, room); - await this.transfer(room, guest, { - roomId: room._id, - transferredBy, - departmentId: guest.department, - }); - } - }, - async savePageHistory(token, roomId, pageInfo) { Livechat.logger.debug(`Saving page movement history for visitor with token ${token}`); if (pageInfo.change !== settings.get('Livechat_history_monitor_type')) { @@ -387,23 +283,6 @@ export const Livechat = { await sendMessage(transferredBy, transferMessage, room); }, - async transfer(room, guest, transferData) { - Livechat.logger.debug(`Transfering room ${room._id} [Transfered by: ${transferData?.transferredBy?._id}]`); - if (room.onHold) { - Livechat.logger.debug('Cannot transfer. Room is on hold'); - throw new Error('error-room-onHold'); - } - - if (transferData.departmentId) { - transferData.department = await LivechatDepartmentRaw.findOneById(transferData.departmentId, { - projection: { name: 1 }, - }); - Livechat.logger.debug(`Transfering room ${room._id} to department ${transferData.department?._id}`); - } - - return RoutingManager.transferRoom(room, guest, transferData); - }, - async returnRoomAsInquiry(rid, departmentId, overrideTransferData = {}) { Livechat.logger.debug(`Transfering room ${rid} to ${departmentId ? 'department' : ''} queue`); const room = await LivechatRooms.findOneById(rid); @@ -682,41 +561,6 @@ export const Livechat = { return updateDepartmentAgents(_id, departmentAgents, department.enabled); }, - /* - * @deprecated - Use the equivalent from DepartmentHelpers class - */ - async removeDepartment(_id) { - check(_id, String); - - const departmentRemovalEnabled = settings.get('Omnichannel_enable_department_removal'); - - if (!departmentRemovalEnabled) { - throw new Meteor.Error('department-removal-disabled', 'Department removal is disabled', { - method: 'livechat:removeDepartment', - }); - } - - const department = await LivechatDepartmentRaw.findOneById(_id, { projection: { _id: 1 } }); - - if (!department) { - throw new Meteor.Error('department-not-found', 'Department not found', { - method: 'livechat:removeDepartment', - }); - } - const ret = (await LivechatDepartmentRaw.removeById(_id)).deletedCount; - const agentsIds = (await LivechatDepartmentAgents.findByDepartmentId(_id, { projection: { agentId: 1 } }).toArray()).map( - (agent) => agent.agentId, - ); - await LivechatDepartmentAgents.removeByDepartmentId(_id); - await LivechatDepartmentRaw.unsetFallbackDepartmentByDepartmentId(_id); - if (ret) { - setImmediate(() => { - callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds }); - }); - } - return ret; - }, - showConnecting() { const { showConnecting } = RoutingManager.getConfig(); return showConnecting; @@ -778,63 +622,6 @@ export const Livechat = { await LivechatRooms.updateVisitorStatus(token, status); }, - async sendOfflineMessage(data = {}) { - if (!settings.get('Livechat_display_offline_form')) { - throw new Error('error-offline-form-disabled'); - } - - const { message, name, email, department, host } = data; - const emailMessage = `${message}`.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1
$2'); - - let html = '

New livechat message

'; - if (host && host !== '') { - html = html.concat(`

Sent from: ${host}

`); - } - html = html.concat(` -

Visitor name: ${name}

-

Visitor email: ${email}

-

Message:
${emailMessage}

`); - - let fromEmail = settings.get('From_Email').match(/\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}\b/i); - - if (fromEmail) { - fromEmail = fromEmail[0]; - } else { - fromEmail = settings.get('From_Email'); - } - - if (settings.get('Livechat_validate_offline_email')) { - const emailDomain = email.substr(email.lastIndexOf('@') + 1); - - try { - await dnsResolveMx(emailDomain); - } catch (e) { - throw new Meteor.Error('error-invalid-email-address', 'Invalid email address', { - method: 'livechat:sendOfflineMessage', - }); - } - } - - // TODO Block offline form if Livechat_offline_email is undefined - // (it does not make sense to have an offline form that does nothing) - // `this.sendEmail` will throw an error if the email is invalid - // thus this breaks livechat, since the "to" email is invalid, and that returns an [invalid email] error to the livechat client - let emailTo = settings.get('Livechat_offline_email'); - if (department && department !== '') { - const dep = await LivechatDepartmentRaw.findOneByIdOrName(department); - emailTo = dep.email || emailTo; - } - - const from = `${name} - ${email} <${fromEmail}>`; - const replyTo = `${name} <${email}>`; - const subject = `Livechat offline message from ${name}: ${`${emailMessage}`.substring(0, 20)}`; - await this.sendEmail(from, emailTo, replyTo, subject, html); - - setImmediate(() => { - callbacks.run('livechat.offlineMessage', data); - }); - }, - async allowAgentChangeServiceStatus(statusLivechat, agentId) { if (statusLivechat !== 'available') { return true; diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index afb649488300..293b15e8d63c 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -1,3 +1,6 @@ +import dns from 'dns'; +import * as util from 'util'; + import { Message, VideoConf, api } from '@rocket.chat/core-services'; import type { IOmnichannelRoom, @@ -10,6 +13,8 @@ import type { ILivechatAgent, IMessage, ILivechatDepartment, + AtLeast, + TransferData, } from '@rocket.chat/core-typings'; import { UserStatus, isOmnichannelRoom } from '@rocket.chat/core-typings'; import { Logger, type MainLogger } from '@rocket.chat/logger'; @@ -24,6 +29,7 @@ import { LivechatDepartmentAgents, ReadReceipts, Rooms, + Settings, } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; @@ -41,7 +47,7 @@ import * as Mailer from '../../../mailer/server/api'; import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; import { getTimezone } from '../../../utils/server/lib/getTimezone'; -import { updateDepartmentAgents, validateEmail } from './Helper'; +import { updateDepartmentAgents, validateEmail, normalizeTransferredByData } from './Helper'; import { QueueManager } from './QueueManager'; import { RoutingManager } from './RoutingManager'; @@ -75,6 +81,16 @@ export type CloseRoomParamsByVisitor = { export type CloseRoomParams = CloseRoomParamsByUser | CloseRoomParamsByVisitor; +type OfflineMessageData = { + message: string; + name: string; + email: string; + department?: string; + host?: string; +}; + +const dnsResolveMx = util.promisify(dns.resolveMx); + class LivechatClass { logger: Logger; @@ -917,6 +933,196 @@ class LivechatClass { await Promise.all([LivechatDepartmentAgents.enableAgentsByDepartmentId(_id), LivechatDepartment.unarchiveDepartment(_id)]); return true; } + + async updateMessage({ guest, message }: { guest: ILivechatVisitor; message: AtLeast }) { + check(message, Match.ObjectIncluding({ _id: String })); + + const originalMessage = await Messages.findOneById>(message._id, { projection: { u: 1 } }); + if (!originalMessage?._id) { + return; + } + + const editAllowed = settings.get('Message_AllowEditing'); + const editOwn = originalMessage.u && originalMessage.u._id === guest._id; + + if (!editAllowed || !editOwn) { + throw new Error('error-action-not-allowed'); + } + + // TODO: Apps sends an `any` object and apparently we just check for _id being present + // while updateMessage expects AtLeast + await updateMessage(message, guest as unknown as IUser); + + return true; + } + + async closeOpenChats(userId: string, comment?: string) { + this.logger.debug(`Closing open chats for user ${userId}`); + const user = await Users.findOneById(userId); + + const extraQuery = await callbacks.run('livechat.applyDepartmentRestrictions', {}, { userId }); + const openChats = LivechatRooms.findOpenByAgent(userId, extraQuery); + const promises: Promise[] = []; + await openChats.forEach((room) => { + promises.push(this.closeRoom({ user, room, comment })); + }); + + await Promise.all(promises); + } + + async transfer(room: IOmnichannelRoom, guest: ILivechatVisitor, transferData: TransferData) { + this.logger.debug(`Transfering room ${room._id} [Transfered by: ${transferData?.transferredBy?._id}]`); + if (room.onHold) { + throw new Error('error-room-onHold'); + } + + if (transferData.departmentId) { + const department = await LivechatDepartment.findOneById(transferData.departmentId, { + projection: { name: 1 }, + }); + if (!department) { + throw new Error('error-invalid-department'); + } + + transferData.department = department; + this.logger.debug(`Transfering room ${room._id} to department ${transferData.department?._id}`); + } + + return RoutingManager.transferRoom(room, guest, transferData); + } + + async forwardOpenChats(userId: string) { + this.logger.debug(`Transferring open chats for user ${userId}`); + const user = await Users.findOneById(userId); + if (!user) { + throw new Error('error-invalid-user'); + } + + const { _id, username, name } = user; + for await (const room of LivechatRooms.findOpenByAgent(userId)) { + const guest = await LivechatVisitors.findOneEnabledById(room.v._id); + if (!guest) { + continue; + } + + const transferredBy = normalizeTransferredByData({ _id, username, name }, room); + await this.transfer(room, guest, { + transferredBy, + departmentId: guest.department, + }); + } + } + + showConnecting() { + return RoutingManager.getConfig()?.showConnecting || false; + } + + async getInitSettings() { + const rcSettings: Record = {}; + + await Settings.findNotHiddenPublic([ + 'Livechat_title', + 'Livechat_title_color', + 'Livechat_enable_message_character_limit', + 'Livechat_message_character_limit', + 'Message_MaxAllowedSize', + 'Livechat_enabled', + 'Livechat_registration_form', + 'Livechat_allow_switching_departments', + 'Livechat_offline_title', + 'Livechat_offline_title_color', + 'Livechat_offline_message', + 'Livechat_offline_success_message', + 'Livechat_offline_form_unavailable', + 'Livechat_display_offline_form', + 'Omnichannel_call_provider', + 'Language', + 'Livechat_enable_transcript', + 'Livechat_transcript_message', + 'Livechat_fileupload_enabled', + 'FileUpload_Enabled', + 'Livechat_conversation_finished_message', + 'Livechat_conversation_finished_text', + 'Livechat_name_field_registration_form', + 'Livechat_email_field_registration_form', + 'Livechat_registration_form_message', + 'Livechat_force_accept_data_processing_consent', + 'Livechat_data_processing_consent_text', + 'Livechat_show_agent_info', + 'Livechat_clear_local_storage_when_chat_ended', + ]).forEach((setting) => { + rcSettings[setting._id] = setting.value; + }); + + rcSettings.Livechat_history_monitor_type = settings.get('Livechat_history_monitor_type'); + + rcSettings.Livechat_Show_Connecting = this.showConnecting(); + + return rcSettings; + } + + async sendOfflineMessage(data: OfflineMessageData) { + if (!settings.get('Livechat_display_offline_form')) { + throw new Error('error-offline-form-disabled'); + } + + const { message, name, email, department, host } = data; + + if (!email) { + throw new Error('error-invalid-email'); + } + + const emailMessage = `${message}`.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1
$2'); + + let html = '

New livechat message

'; + if (host && host !== '') { + html = html.concat(`

Sent from: ${host}

`); + } + html = html.concat(` +

Visitor name: ${name}

+

Visitor email: ${email}

+

Message:
${emailMessage}

`); + + const fromEmail = settings.get('From_Email').match(/\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}\b/i); + + let from: string; + if (fromEmail) { + from = fromEmail[0]; + } else { + from = settings.get('From_Email'); + } + + if (settings.get('Livechat_validate_offline_email')) { + const emailDomain = email.substr(email.lastIndexOf('@') + 1); + + try { + await dnsResolveMx(emailDomain); + } catch (e) { + throw new Meteor.Error('error-invalid-email-address'); + } + } + + // TODO Block offline form if Livechat_offline_email is undefined + // (it does not make sense to have an offline form that does nothing) + // `this.sendEmail` will throw an error if the email is invalid + // thus this breaks livechat, since the "to" email is invalid, and that returns an [invalid email] error to the livechat client + let emailTo = settings.get('Livechat_offline_email'); + if (department && department !== '') { + const dep = await LivechatDepartment.findOneByIdOrName(department, { projection: { email: 1 } }); + if (dep) { + emailTo = dep.email || emailTo; + } + } + + const fromText = `${name} - ${email} <${from}>`; + const replyTo = `${name} <${email}>`; + const subject = `Livechat offline message from ${name}: ${`${emailMessage}`.substring(0, 20)}`; + await this.sendEmail(fromText, emailTo, replyTo, subject, html); + + setImmediate(() => { + void callbacks.run('livechat.offlineMessage', data); + }); + } } export const Livechat = new LivechatClass(); diff --git a/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts b/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts index bbce5d16efb4..5ddd25e90bd2 100644 --- a/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts +++ b/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts @@ -1,7 +1,7 @@ import { Logger } from '@rocket.chat/logger'; import { settings } from '../../../../settings/server'; -import { Livechat } from '../Livechat'; +import { Livechat } from '../LivechatTyped'; const logger = new Logger('AgentStatusWatcher'); diff --git a/apps/meteor/app/livechat/server/methods/sendOfflineMessage.ts b/apps/meteor/app/livechat/server/methods/sendOfflineMessage.ts index 9a475de5e32d..c3b5537f31be 100644 --- a/apps/meteor/app/livechat/server/methods/sendOfflineMessage.ts +++ b/apps/meteor/app/livechat/server/methods/sendOfflineMessage.ts @@ -4,7 +4,7 @@ import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; import { Meteor } from 'meteor/meteor'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { Livechat } from '../lib/Livechat'; +import { Livechat } from '../lib/LivechatTyped'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/apps/meteor/app/livechat/server/methods/setDepartmentForVisitor.ts b/apps/meteor/app/livechat/server/methods/setDepartmentForVisitor.ts index 61e6b21267da..a14933ed8d47 100644 --- a/apps/meteor/app/livechat/server/methods/setDepartmentForVisitor.ts +++ b/apps/meteor/app/livechat/server/methods/setDepartmentForVisitor.ts @@ -5,7 +5,7 @@ import { Meteor } from 'meteor/meteor'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { normalizeTransferredByData } from '../lib/Helper'; -import { Livechat } from '../lib/Livechat'; +import { Livechat } from '../lib/LivechatTyped'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/apps/meteor/app/livechat/server/methods/transfer.ts b/apps/meteor/app/livechat/server/methods/transfer.ts index 3817b10bf42b..16ee1abc6191 100644 --- a/apps/meteor/app/livechat/server/methods/transfer.ts +++ b/apps/meteor/app/livechat/server/methods/transfer.ts @@ -7,7 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { normalizeTransferredByData } from '../lib/Helper'; -import { Livechat } from '../lib/Livechat'; +import { Livechat } from '../lib/LivechatTyped'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -60,6 +60,10 @@ Meteor.methods({ const guest = await LivechatVisitors.findOneEnabledById(room.v?._id); + if (!guest) { + throw new Meteor.Error('error-invalid-visitor', 'Invalid visitor', { method: 'livechat:transfer' }); + } + const user = await Meteor.userAsync(); if (!user) { diff --git a/packages/core-typings/src/omnichannel/routing.ts b/packages/core-typings/src/omnichannel/routing.ts index eed6dd6f1a19..43ca0c08f5d2 100644 --- a/packages/core-typings/src/omnichannel/routing.ts +++ b/packages/core-typings/src/omnichannel/routing.ts @@ -24,7 +24,7 @@ export interface IRoutingMethod { } export type TransferData = { - userId: string; + userId?: string; departmentId?: string; department?: Pick; transferredBy: { @@ -36,7 +36,7 @@ export type TransferData = { name?: string; }; clientAction?: boolean; - scope: 'agent' | 'department' | 'queue' | 'autoTransferUnansweredChatsToAgent' | 'autoTransferUnansweredChatsToQueue'; + scope?: 'agent' | 'department' | 'queue' | 'autoTransferUnansweredChatsToAgent' | 'autoTransferUnansweredChatsToQueue'; comment?: string; }; From 3b5310cf2350b93ab5f171d5d547434e6a9f46c5 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 18 Oct 2023 05:48:04 -0700 Subject: [PATCH 20/38] regression: Restore default limits to community apps (#30611) Co-authored-by: Rodrigo Nascimento --- ee/packages/license/src/deprecated.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ee/packages/license/src/deprecated.ts b/ee/packages/license/src/deprecated.ts index 65851a79c7eb..0a4a6b0f1bb3 100644 --- a/ee/packages/license/src/deprecated.ts +++ b/ee/packages/license/src/deprecated.ts @@ -23,8 +23,8 @@ export function getMaxActiveUsers(this: LicenseManager) { export function getAppsConfig(this: LicenseManager) { return { - maxPrivateApps: getLicenseLimit(this.getLicense(), 'privateApps') ?? -1, - maxMarketplaceApps: getLicenseLimit(this.getLicense(), 'marketplaceApps') ?? -1, + maxPrivateApps: getLicenseLimit(this.getLicense(), 'privateApps') ?? 3, + maxMarketplaceApps: getLicenseLimit(this.getLicense(), 'marketplaceApps') ?? 5, }; } From 343ba56f44c35ea7044c73f3f9e9af8debd19a79 Mon Sep 17 00:00:00 2001 From: Rafael Tapia Date: Wed, 18 Oct 2023 10:34:02 -0300 Subject: [PATCH 21/38] test: wait for the name update finish (#30663) --- apps/meteor/tests/end-to-end/api/09-rooms.js | 47 ++++++++++---------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/09-rooms.js b/apps/meteor/tests/end-to-end/api/09-rooms.js index ed3c7eefb15b..10d576c316a2 100644 --- a/apps/meteor/tests/end-to-end/api/09-rooms.js +++ b/apps/meteor/tests/end-to-end/api/09-rooms.js @@ -1570,29 +1570,30 @@ describe('[Rooms]', function () { }); }); - it('should update group name if user changes name', (done) => { - updateSetting('UI_Use_Real_Name', true).then(() => { - request - .post(api('users.update')) - .set(credentials) - .send({ - userId: testUser._id, - data: { - name: `changed.name.${testUser.username}`, - }, - }) - .end(() => { - request - .get(api('subscriptions.getOne')) - .set(credentials) - .query({ roomId }) - .end((err, res) => { - const { subscription } = res.body; - expect(subscription.fname).to.equal(`changed.name.${testUser.username}, Rocket.Cat`); - done(); - }); - }); - }); + it('should update group name if user changes name', async () => { + await updateSetting('UI_Use_Real_Name', true); + await request + .post(api('users.update')) + .set(credentials) + .send({ + userId: testUser._id, + data: { + name: `changed.name.${testUser.username}`, + }, + }); + + // need to wait for the name update finish + await sleep(300); + + await request + .get(api('subscriptions.getOne')) + .set(credentials) + .query({ roomId }) + .send() + .expect((res) => { + const { subscription } = res.body; + expect(subscription.fname).to.equal(`changed.name.${testUser.username}, Rocket.Cat`); + }); }); }); From 049b921bc337d1ec802d60f1493414ccac5b4972 Mon Sep 17 00:00:00 2001 From: Noach Magedman Date: Wed, 18 Oct 2023 17:10:01 +0300 Subject: [PATCH 22/38] fix: Handle AWS S3 Re-Authentication via s3.getSignedUrlPromise (#30642) --- apps/meteor/app/file-upload/ufs/AmazonS3/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/file-upload/ufs/AmazonS3/server.ts b/apps/meteor/app/file-upload/ufs/AmazonS3/server.ts index b9f0807b6112..d6b69faf75fa 100644 --- a/apps/meteor/app/file-upload/ufs/AmazonS3/server.ts +++ b/apps/meteor/app/file-upload/ufs/AmazonS3/server.ts @@ -80,7 +80,7 @@ class AmazonS3Store extends UploadFS.Store { ResponseContentDisposition: `${forceDownload ? 'attachment' : 'inline'}; filename="${encodeURI(file.name || '')}"`, }; - return s3.getSignedUrl('getObject', params); + return s3.getSignedUrlPromise('getObject', params); }; /** From e24d071675c720d8dc947193b180ee2c81cde95b Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Wed, 18 Oct 2023 13:17:28 -0300 Subject: [PATCH 23/38] fix: inconsistent behavior when removing subscriptions and inquiries (#30572) --- .changeset/long-cars-dream.md | 5 +++ .../client/lib/stream/queueManager.ts | 26 ++++++++++--- .../client/views/room/hooks/useOpenRoom.ts | 10 +++++ .../views/room/providers/RoomProvider.tsx | 39 +------------------ 4 files changed, 38 insertions(+), 42 deletions(-) create mode 100644 .changeset/long-cars-dream.md diff --git a/.changeset/long-cars-dream.md b/.changeset/long-cars-dream.md new file mode 100644 index 000000000000..95f226d6dfb4 --- /dev/null +++ b/.changeset/long-cars-dream.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed intermittent errors caused by the removal of subscriptions and inquiries when lacking permissions. diff --git a/apps/meteor/app/livechat/client/lib/stream/queueManager.ts b/apps/meteor/app/livechat/client/lib/stream/queueManager.ts index 28d09958535a..906ace402bb9 100644 --- a/apps/meteor/app/livechat/client/lib/stream/queueManager.ts +++ b/apps/meteor/app/livechat/client/lib/stream/queueManager.ts @@ -8,18 +8,34 @@ import { LivechatInquiry } from '../../collections/LivechatInquiry'; const departments = new Set(); const events = { - added: (inquiry: ILivechatInquiryRecord) => { - departments.has(inquiry.department) && LivechatInquiry.insert({ ...inquiry, alert: true, _updatedAt: new Date(inquiry._updatedAt) }); + added: async (inquiry: ILivechatInquiryRecord) => { + if (!departments.has(inquiry.department)) { + return; + } + + LivechatInquiry.insert({ ...inquiry, alert: true, _updatedAt: new Date(inquiry._updatedAt) }); + await invalidateRoomQueries(inquiry.rid); }, changed: async (inquiry: ILivechatInquiryRecord) => { if (inquiry.status !== 'queued' || (inquiry.department && !departments.has(inquiry.department))) { - return LivechatInquiry.remove(inquiry._id); + return removeInquiry(inquiry); } LivechatInquiry.upsert({ _id: inquiry._id }, { ...inquiry, alert: true, _updatedAt: new Date(inquiry._updatedAt) }); - await queryClient.invalidateQueries(['/v1/rooms.info', inquiry.rid]); + await invalidateRoomQueries(inquiry.rid); }, - removed: (inquiry: ILivechatInquiryRecord) => LivechatInquiry.remove(inquiry._id), + removed: (inquiry: ILivechatInquiryRecord) => removeInquiry(inquiry), +}; + +const invalidateRoomQueries = async (rid: string) => { + await queryClient.invalidateQueries(['rooms', { reference: rid, type: 'l' }]); + await queryClient.removeQueries(['rooms', rid]); + await queryClient.removeQueries(['/v1/rooms.info', rid]); +}; + +const removeInquiry = async (inquiry: ILivechatInquiryRecord) => { + await LivechatInquiry.remove(inquiry._id); + return queryClient.invalidateQueries(['rooms', { reference: inquiry.rid, type: 'l' }]); }; const getInquiriesFromAPI = async () => { diff --git a/apps/meteor/client/views/room/hooks/useOpenRoom.ts b/apps/meteor/client/views/room/hooks/useOpenRoom.ts index c2b694414002..d529145aaf17 100644 --- a/apps/meteor/client/views/room/hooks/useOpenRoom.ts +++ b/apps/meteor/client/views/room/hooks/useOpenRoom.ts @@ -8,6 +8,7 @@ import { omit } from '../../../../lib/utils/omit'; import { NotAuthorizedError } from '../../../lib/errors/NotAuthorizedError'; import { OldUrlRoomError } from '../../../lib/errors/OldUrlRoomError'; import { RoomNotFoundError } from '../../../lib/errors/RoomNotFoundError'; +import { queryClient } from '../../../lib/queryClient'; export function useOpenRoom({ type, reference }: { type: RoomType; reference: string }) { const user = useUser(); @@ -102,6 +103,15 @@ export function useOpenRoom({ type, reference }: { type: RoomType; reference: st }, { retry: 0, + onError: async (error) => { + if (['l', 'v'].includes(type) && error instanceof RoomNotFoundError) { + const { ChatRoom } = await import('../../../../app/models/client'); + + ChatRoom.remove(reference); + queryClient.removeQueries(['rooms', reference]); + queryClient.removeQueries(['/v1/rooms.info', reference]); + } + }, }, ); } diff --git a/apps/meteor/client/views/room/providers/RoomProvider.tsx b/apps/meteor/client/views/room/providers/RoomProvider.tsx index e19fa8136f59..82c66c6f5d8d 100644 --- a/apps/meteor/client/views/room/providers/RoomProvider.tsx +++ b/apps/meteor/client/views/room/providers/RoomProvider.tsx @@ -1,10 +1,9 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { usePermission, useStream, useUserId, useRouter } from '@rocket.chat/ui-contexts'; -import { useQueryClient } from '@tanstack/react-query'; +import { useRouter } from '@rocket.chat/ui-contexts'; import type { ReactNode, ContextType, ReactElement } from 'react'; import React, { useMemo, memo, useEffect, useCallback } from 'react'; -import { ChatRoom, ChatSubscription } from '../../../../app/models/client'; +import { ChatSubscription } from '../../../../app/models/client'; import { RoomHistoryManager } from '../../../../app/ui-utils/client'; import { UserAction } from '../../../../app/ui/client/lib/UserAction'; import { useReactiveQuery } from '../../../hooks/useReactiveQuery'; @@ -29,24 +28,6 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { const { data: room, isSuccess } = useRoomQuery(rid); - const subscribeToRoom = useStream('room-data'); - - const queryClient = useQueryClient(); - const userId = useUserId(); - const isLivechatAdmin = usePermission('view-livechat-rooms'); - const { t: roomType } = room ?? {}; - - // TODO: move this to omnichannel context only - useEffect(() => { - if (roomType !== 'l') { - return; - } - - return subscribeToRoom(rid, (room) => { - queryClient.setQueryData(['rooms', rid], room); - }); - }, [subscribeToRoom, rid, queryClient, roomType]); - // TODO: the following effect is a workaround while we don't have a general and definitive solution for it const router = useRouter(); useEffect(() => { @@ -55,22 +36,6 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { } }, [isSuccess, room, router]); - const { _id: servedById } = room?.servedBy ?? {}; - - // TODO: Review the necessity of this effect when we move away from cached collections - useEffect(() => { - if (roomType !== 'l' || !servedById) { - return; - } - - if (!isLivechatAdmin && servedById !== userId) { - ChatRoom.remove(rid); - queryClient.removeQueries(['rooms', rid]); - queryClient.removeQueries(['rooms', { reference: rid, type: 'l' }]); - queryClient.removeQueries(['/v1/rooms.info', rid]); - } - }, [isLivechatAdmin, queryClient, userId, rid, roomType, servedById]); - const subscriptionQuery = useReactiveQuery(['subscriptions', { rid }], () => ChatSubscription.findOne({ rid }) ?? null); const pseudoRoom = useMemo(() => { From f3dd1277e6ff7010bb6251b9b6554146a8c40a7c Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Wed, 18 Oct 2023 15:05:02 -0300 Subject: [PATCH 24/38] feat: Added new setting 'Hide conversation after closing' (#30591) --- .changeset/thick-spoons-compete.md | 5 +++ .../omnichannel/useOmnichannelCloseRoute.ts | 23 ++++++++++++ .../OmnichannelPreferencesPage.tsx | 5 ++- .../omnichannel/PreferencesGeneral.tsx | 26 +++++++++++++ .../room/body/hooks/useGoToHomeOnRemoved.ts | 37 ++++++++++++++----- .../rocketchat-i18n/i18n/en.i18n.json | 2 + .../rocketchat-i18n/i18n/pt-BR.i18n.json | 2 + .../server/methods/saveUserPreferences.ts | 1 + .../v1/users/UsersSetPreferenceParamsPOST.ts | 5 +++ 9 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 .changeset/thick-spoons-compete.md create mode 100644 apps/meteor/client/hooks/omnichannel/useOmnichannelCloseRoute.ts create mode 100644 apps/meteor/client/views/account/omnichannel/PreferencesGeneral.tsx diff --git a/.changeset/thick-spoons-compete.md b/.changeset/thick-spoons-compete.md new file mode 100644 index 000000000000..cf6e9eb2697d --- /dev/null +++ b/.changeset/thick-spoons-compete.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Added new Omnichannel setting 'Hide conversation after closing' diff --git a/apps/meteor/client/hooks/omnichannel/useOmnichannelCloseRoute.ts b/apps/meteor/client/hooks/omnichannel/useOmnichannelCloseRoute.ts new file mode 100644 index 000000000000..746a62bd87e6 --- /dev/null +++ b/apps/meteor/client/hooks/omnichannel/useOmnichannelCloseRoute.ts @@ -0,0 +1,23 @@ +import { useRouter, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useCallback } from 'react'; + +export const useOmnichannelCloseRoute = () => { + const hideConversationAfterClosing = useUserPreference('omnichannelHideConversationAfterClosing') ?? true; + const router = useRouter(); + + const navigateHome = useCallback(() => { + if (!hideConversationAfterClosing) { + return; + } + + const routeName = router.getRouteName(); + + if (routeName === 'omnichannel-current-chats') { + router.navigate({ name: 'omnichannel-current-chats' }); + } else { + router.navigate({ name: 'home' }); + } + }, [hideConversationAfterClosing, router]); + + return { navigateHome }; +}; diff --git a/apps/meteor/client/views/account/omnichannel/OmnichannelPreferencesPage.tsx b/apps/meteor/client/views/account/omnichannel/OmnichannelPreferencesPage.tsx index 515446a154f6..d448a180f834 100644 --- a/apps/meteor/client/views/account/omnichannel/OmnichannelPreferencesPage.tsx +++ b/apps/meteor/client/views/account/omnichannel/OmnichannelPreferencesPage.tsx @@ -6,6 +6,7 @@ import { useForm, FormProvider } from 'react-hook-form'; import Page from '../../../components/Page'; import PreferencesConversationTranscript from './PreferencesConversationTranscript'; +import { PreferencesGeneral } from './PreferencesGeneral'; type FormData = { omnichannelTranscriptPDF: boolean; @@ -18,9 +19,10 @@ const OmnichannelPreferencesPage = (): ReactElement => { const omnichannelTranscriptPDF = useUserPreference('omnichannelTranscriptPDF') ?? false; const omnichannelTranscriptEmail = useUserPreference('omnichannelTranscriptEmail') ?? false; + const omnichannelHideConversationAfterClosing = useUserPreference('omnichannelHideConversationAfterClosing') ?? true; const methods = useForm({ - defaultValues: { omnichannelTranscriptPDF, omnichannelTranscriptEmail }, + defaultValues: { omnichannelTranscriptPDF, omnichannelTranscriptEmail, omnichannelHideConversationAfterClosing }, }); const { @@ -48,6 +50,7 @@ const OmnichannelPreferencesPage = (): ReactElement => { + diff --git a/apps/meteor/client/views/account/omnichannel/PreferencesGeneral.tsx b/apps/meteor/client/views/account/omnichannel/PreferencesGeneral.tsx new file mode 100644 index 000000000000..67c06bd2c5b2 --- /dev/null +++ b/apps/meteor/client/views/account/omnichannel/PreferencesGeneral.tsx @@ -0,0 +1,26 @@ +import { Box, Field, FieldGroup, FieldHint, FieldLabel, FieldRow, ToggleSwitch } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React from 'react'; +import { useFormContext } from 'react-hook-form'; + +export const PreferencesGeneral = (): ReactElement => { + const t = useTranslation(); + const { register } = useFormContext(); + const omnichannelHideAfterClosing = useUniqueId(); + + return ( + + + + {t('Omnichannel_hide_conversation_after_closing')} + + + + + {t('Omnichannel_hide_conversation_after_closing_description')} + + + ); +}; diff --git a/apps/meteor/client/views/room/body/hooks/useGoToHomeOnRemoved.ts b/apps/meteor/client/views/room/body/hooks/useGoToHomeOnRemoved.ts index a087d288d0d7..068c97a2de4b 100644 --- a/apps/meteor/client/views/room/body/hooks/useGoToHomeOnRemoved.ts +++ b/apps/meteor/client/views/room/body/hooks/useGoToHomeOnRemoved.ts @@ -1,9 +1,9 @@ -import type { IRoom } from '@rocket.chat/core-typings'; +import { isOmnichannelRoom, type IRoom } from '@rocket.chat/core-typings'; import { useRoute, useStream, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import { useQueryClient } from '@tanstack/react-query'; import { useEffect } from 'react'; -const IGNORED_ROOMS = ['l', 'v']; +import { useOmnichannelCloseRoute } from '../../../../hooks/omnichannel/useOmnichannelCloseRoute'; export function useGoToHomeOnRemoved(room: IRoom, userId: string | undefined): void { const homeRouter = useRoute('home'); @@ -11,6 +11,7 @@ export function useGoToHomeOnRemoved(room: IRoom, userId: string | undefined): v const dispatchToastMessage = useToastMessageDispatch(); const subscribeToNotifyUser = useStream('notify-user'); const t = useTranslation(); + const { navigateHome } = useOmnichannelCloseRoute(); useEffect(() => { if (!userId) { @@ -21,19 +22,35 @@ export function useGoToHomeOnRemoved(room: IRoom, userId: string | undefined): v if (event === 'removed' && subscription.rid === room._id) { queryClient.invalidateQueries(['rooms', room._id]); - if (!IGNORED_ROOMS.includes(room.t)) { - dispatchToastMessage({ - type: 'info', - message: t('You_have_been_removed_from__roomName_', { - roomName: room?.fname || room?.name || '', - }), - }); + if (isOmnichannelRoom(room)) { + navigateHome(); + return; } + dispatchToastMessage({ + type: 'info', + message: t('You_have_been_removed_from__roomName_', { + roomName: room?.fname || room?.name || '', + }), + }); + homeRouter.push({}); } }); return unSubscribeFromNotifyUser; - }, [userId, homeRouter, subscribeToNotifyUser, room._id, room?.fname, room?.name, t, dispatchToastMessage, queryClient, room.t]); + }, [ + userId, + homeRouter, + subscribeToNotifyUser, + room._id, + room?.fname, + room?.name, + t, + dispatchToastMessage, + queryClient, + room.t, + room, + navigateHome, + ]); } diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 001cdf080f7b..7ef10e0988b0 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -3121,6 +3121,8 @@ "Omnichannel_sorting_disclaimer": "Omnichannel conversations are sorted by {{sortingMechanism}}, edit a room to apply.", "Livechat_online": "Omnichannel on-line", "Omnichannel_placed_chat_on_hold": "Chat On Hold: {{comment}}", + "Omnichannel_hide_conversation_after_closing": "Hide conversation after closing", + "Omnichannel_hide_conversation_after_closing_description": "After closing the conversation you will be redirected to Home.", "Livechat_Queue": "Omnichannel Queue", "Livechat_registration_form": "Registration Form", "Livechat_registration_form_message": "Registration Form Message", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 7dd1b47f335e..8da04e7c4e67 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -2679,6 +2679,8 @@ "Omnichannel_sorting_disclaimer": "Conversar do Omnichannel são ordenadas por {{sortingMechanism}}, edite a sala para alterar.", "Livechat_online": "Omnichannel online", "Omnichannel_placed_chat_on_hold": "Conversa em espera: {{comment}}", + "Omnichannel_hide_conversation_after_closing": "Ocultar conversa após fechar", + "Omnichannel_hide_conversation_after_closing_description": "Após encerrar a conversa, você será redirecionado para a página inicial.", "Livechat_Queue": "Fila omnichannel", "Livechat_registration_form": "Formulário de registro", "Livechat_registration_form_message": "Mensagem do formulário de registro", diff --git a/apps/meteor/server/methods/saveUserPreferences.ts b/apps/meteor/server/methods/saveUserPreferences.ts index 71abe7bea3b1..814627a745bc 100644 --- a/apps/meteor/server/methods/saveUserPreferences.ts +++ b/apps/meteor/server/methods/saveUserPreferences.ts @@ -86,6 +86,7 @@ export const saveUserPreferences = async (settings: Partial, us fontSize: Match.Optional(String), omnichannelTranscriptEmail: Match.Optional(Boolean), omnichannelTranscriptPDF: Match.Optional(Boolean), + omnichannelHideConversationAfterClosing: Match.Optional(Boolean), notifyCalendarEvents: Match.Optional(Boolean), enableMobileRinging: Match.Optional(Boolean), mentionsWithSymbol: Match.Optional(Boolean), diff --git a/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts b/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts index bb32dc27fb04..1c89fdc04d5d 100644 --- a/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts +++ b/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts @@ -49,6 +49,7 @@ export type UsersSetPreferencesParamsPOST = { idleTimeLimit?: number; omnichannelTranscriptEmail?: boolean; omnichannelTranscriptPDF?: boolean; + omnichannelHideConversationAfterClosing?: boolean; enableMobileRinging?: boolean; mentionsWithSymbol?: boolean; }; @@ -242,6 +243,10 @@ const UsersSetPreferencesParamsPostSchema = { type: 'boolean', nullable: true, }, + omnichannelHideConversationAfterClosing: { + type: 'boolean', + nullable: true, + }, enableMobileRinging: { type: 'boolean', nullable: true, From 083840662c1cf79633a12889663f4414517c6e9a Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 18 Oct 2023 13:07:08 -0600 Subject: [PATCH 25/38] refactor: Small files to typescript (#30665) --- .../federation/server/lib/getFederationDiscoveryMethod.js | 3 --- .../federation/server/lib/getFederationDiscoveryMethod.ts | 3 +++ .../meteor/app/federation/server/lib/getFederationDomain.js | 3 --- .../meteor/app/federation/server/lib/getFederationDomain.ts | 3 +++ .../meteor/app/federation/server/lib/isFederationEnabled.js | 3 --- .../meteor/app/federation/server/lib/isFederationEnabled.ts | 3 +++ .../app/federation/server/lib/{logger.js => logger.ts} | 0 apps/meteor/definition/externals/meteor/meteor.d.ts | 6 ++++++ .../lib/{addRoleRestrictions.js => addRoleRestrictions.ts} | 0 .../lib/{guestPermissions.js => guestPermissions.ts} | 0 apps/meteor/ee/app/settings/server/{index.js => index.ts} | 0 apps/meteor/server/startup/{appcache.js => appcache.ts} | 0 apps/meteor/server/startup/migrations/{xrun.js => xrun.ts} | 0 apps/meteor/tests/data/api-data.js | 4 ++-- apps/meteor/tests/data/{channel.js => channel.ts} | 0 apps/meteor/tests/data/{interactions.js => interactions.ts} | 0 apps/meteor/tests/data/{role.js => role.ts} | 0 apps/meteor/tests/data/uploads.helper.ts | 2 +- apps/meteor/tests/end-to-end/api/01-users.js | 2 +- apps/meteor/tests/end-to-end/api/09-rooms.js | 2 +- apps/meteor/tests/end-to-end/api/14-assets.js | 2 +- 21 files changed, 21 insertions(+), 15 deletions(-) delete mode 100644 apps/meteor/app/federation/server/lib/getFederationDiscoveryMethod.js create mode 100644 apps/meteor/app/federation/server/lib/getFederationDiscoveryMethod.ts delete mode 100644 apps/meteor/app/federation/server/lib/getFederationDomain.js create mode 100644 apps/meteor/app/federation/server/lib/getFederationDomain.ts delete mode 100644 apps/meteor/app/federation/server/lib/isFederationEnabled.js create mode 100644 apps/meteor/app/federation/server/lib/isFederationEnabled.ts rename apps/meteor/app/federation/server/lib/{logger.js => logger.ts} (100%) rename apps/meteor/ee/app/authorization/lib/{addRoleRestrictions.js => addRoleRestrictions.ts} (100%) rename apps/meteor/ee/app/authorization/lib/{guestPermissions.js => guestPermissions.ts} (100%) rename apps/meteor/ee/app/settings/server/{index.js => index.ts} (100%) rename apps/meteor/server/startup/{appcache.js => appcache.ts} (100%) rename apps/meteor/server/startup/migrations/{xrun.js => xrun.ts} (100%) rename apps/meteor/tests/data/{channel.js => channel.ts} (100%) rename apps/meteor/tests/data/{interactions.js => interactions.ts} (100%) rename apps/meteor/tests/data/{role.js => role.ts} (100%) diff --git a/apps/meteor/app/federation/server/lib/getFederationDiscoveryMethod.js b/apps/meteor/app/federation/server/lib/getFederationDiscoveryMethod.js deleted file mode 100644 index 2da490942fdc..000000000000 --- a/apps/meteor/app/federation/server/lib/getFederationDiscoveryMethod.js +++ /dev/null @@ -1,3 +0,0 @@ -import { settings } from '../../../settings/server'; - -export const getFederationDiscoveryMethod = () => settings.get('FEDERATION_Discovery_Method'); diff --git a/apps/meteor/app/federation/server/lib/getFederationDiscoveryMethod.ts b/apps/meteor/app/federation/server/lib/getFederationDiscoveryMethod.ts new file mode 100644 index 000000000000..b8ea8c4f6ce6 --- /dev/null +++ b/apps/meteor/app/federation/server/lib/getFederationDiscoveryMethod.ts @@ -0,0 +1,3 @@ +import { settings } from '../../../settings/server'; + +export const getFederationDiscoveryMethod = () => settings.get('FEDERATION_Discovery_Method'); diff --git a/apps/meteor/app/federation/server/lib/getFederationDomain.js b/apps/meteor/app/federation/server/lib/getFederationDomain.js deleted file mode 100644 index c5e67629db75..000000000000 --- a/apps/meteor/app/federation/server/lib/getFederationDomain.js +++ /dev/null @@ -1,3 +0,0 @@ -import { settings } from '../../../settings/server'; - -export const getFederationDomain = () => settings.get('FEDERATION_Domain').replace('@', ''); diff --git a/apps/meteor/app/federation/server/lib/getFederationDomain.ts b/apps/meteor/app/federation/server/lib/getFederationDomain.ts new file mode 100644 index 000000000000..80f683743f2d --- /dev/null +++ b/apps/meteor/app/federation/server/lib/getFederationDomain.ts @@ -0,0 +1,3 @@ +import { settings } from '../../../settings/server'; + +export const getFederationDomain = () => settings.get('FEDERATION_Domain').replace('@', ''); diff --git a/apps/meteor/app/federation/server/lib/isFederationEnabled.js b/apps/meteor/app/federation/server/lib/isFederationEnabled.js deleted file mode 100644 index 9e46d3004ace..000000000000 --- a/apps/meteor/app/federation/server/lib/isFederationEnabled.js +++ /dev/null @@ -1,3 +0,0 @@ -import { settings } from '../../../settings/server'; - -export const isFederationEnabled = () => settings.get('FEDERATION_Enabled'); diff --git a/apps/meteor/app/federation/server/lib/isFederationEnabled.ts b/apps/meteor/app/federation/server/lib/isFederationEnabled.ts new file mode 100644 index 000000000000..e3edb818e602 --- /dev/null +++ b/apps/meteor/app/federation/server/lib/isFederationEnabled.ts @@ -0,0 +1,3 @@ +import { settings } from '../../../settings/server'; + +export const isFederationEnabled = () => settings.get('FEDERATION_Enabled'); diff --git a/apps/meteor/app/federation/server/lib/logger.js b/apps/meteor/app/federation/server/lib/logger.ts similarity index 100% rename from apps/meteor/app/federation/server/lib/logger.js rename to apps/meteor/app/federation/server/lib/logger.ts diff --git a/apps/meteor/definition/externals/meteor/meteor.d.ts b/apps/meteor/definition/externals/meteor/meteor.d.ts index 622f3032b484..4854d24a37ba 100644 --- a/apps/meteor/definition/externals/meteor/meteor.d.ts +++ b/apps/meteor/definition/externals/meteor/meteor.d.ts @@ -130,5 +130,11 @@ declare module 'meteor/meteor' { ...args: StringifyBuffers> ) => ReturnType | Promise>; }): void; + + const AppCache: + | { + config: (config: { onlineOnly: string[] }) => void; + } + | undefined; } } diff --git a/apps/meteor/ee/app/authorization/lib/addRoleRestrictions.js b/apps/meteor/ee/app/authorization/lib/addRoleRestrictions.ts similarity index 100% rename from apps/meteor/ee/app/authorization/lib/addRoleRestrictions.js rename to apps/meteor/ee/app/authorization/lib/addRoleRestrictions.ts diff --git a/apps/meteor/ee/app/authorization/lib/guestPermissions.js b/apps/meteor/ee/app/authorization/lib/guestPermissions.ts similarity index 100% rename from apps/meteor/ee/app/authorization/lib/guestPermissions.js rename to apps/meteor/ee/app/authorization/lib/guestPermissions.ts diff --git a/apps/meteor/ee/app/settings/server/index.js b/apps/meteor/ee/app/settings/server/index.ts similarity index 100% rename from apps/meteor/ee/app/settings/server/index.js rename to apps/meteor/ee/app/settings/server/index.ts diff --git a/apps/meteor/server/startup/appcache.js b/apps/meteor/server/startup/appcache.ts similarity index 100% rename from apps/meteor/server/startup/appcache.js rename to apps/meteor/server/startup/appcache.ts diff --git a/apps/meteor/server/startup/migrations/xrun.js b/apps/meteor/server/startup/migrations/xrun.ts similarity index 100% rename from apps/meteor/server/startup/migrations/xrun.js rename to apps/meteor/server/startup/migrations/xrun.ts diff --git a/apps/meteor/tests/data/api-data.js b/apps/meteor/tests/data/api-data.js index 25e89c2ef99a..d08e4cc50c54 100644 --- a/apps/meteor/tests/data/api-data.js +++ b/apps/meteor/tests/data/api-data.js @@ -1,7 +1,7 @@ import supertest from 'supertest'; -import { publicChannelName, privateChannelName } from './channel.js'; -import { roleNameUsers, roleNameSubscriptions, roleScopeUsers, roleScopeSubscriptions, roleDescription } from './role.js'; +import { publicChannelName, privateChannelName } from './channel'; +import { roleNameUsers, roleNameSubscriptions, roleScopeUsers, roleScopeSubscriptions, roleDescription } from './role'; import { username, email, adminUsername, adminPassword } from './user'; const apiUrl = process.env.TEST_API_URL || 'http://localhost:3000'; diff --git a/apps/meteor/tests/data/channel.js b/apps/meteor/tests/data/channel.ts similarity index 100% rename from apps/meteor/tests/data/channel.js rename to apps/meteor/tests/data/channel.ts diff --git a/apps/meteor/tests/data/interactions.js b/apps/meteor/tests/data/interactions.ts similarity index 100% rename from apps/meteor/tests/data/interactions.js rename to apps/meteor/tests/data/interactions.ts diff --git a/apps/meteor/tests/data/role.js b/apps/meteor/tests/data/role.ts similarity index 100% rename from apps/meteor/tests/data/role.js rename to apps/meteor/tests/data/role.ts diff --git a/apps/meteor/tests/data/uploads.helper.ts b/apps/meteor/tests/data/uploads.helper.ts index 194b19df34c8..29c7a143484c 100644 --- a/apps/meteor/tests/data/uploads.helper.ts +++ b/apps/meteor/tests/data/uploads.helper.ts @@ -5,7 +5,7 @@ import { after, before, it } from 'mocha'; import { api, request, credentials } from './api-data.js'; import { password } from './user'; import { createUser, login } from './users.helper'; -import { imgURL } from './interactions.js'; +import { imgURL } from './interactions'; import { updateSetting } from './permissions.helper'; import { createRoom } from './rooms.helper'; import { createVisitor } from './livechat/rooms'; diff --git a/apps/meteor/tests/end-to-end/api/01-users.js b/apps/meteor/tests/end-to-end/api/01-users.js index d99fa68a036f..b8343dc015da 100644 --- a/apps/meteor/tests/end-to-end/api/01-users.js +++ b/apps/meteor/tests/end-to-end/api/01-users.js @@ -19,7 +19,7 @@ import { } from '../../data/api-data.js'; import { MAX_BIO_LENGTH, MAX_NICKNAME_LENGTH } from '../../data/constants.ts'; import { customFieldText, clearCustomFields, setCustomFields } from '../../data/custom-fields.js'; -import { imgURL } from '../../data/interactions.js'; +import { imgURL } from '../../data/interactions'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; import { createRoom } from '../../data/rooms.helper'; import { adminEmail, preferences, password, adminUsername } from '../../data/user'; diff --git a/apps/meteor/tests/end-to-end/api/09-rooms.js b/apps/meteor/tests/end-to-end/api/09-rooms.js index 10d576c316a2..533c0b63da44 100644 --- a/apps/meteor/tests/end-to-end/api/09-rooms.js +++ b/apps/meteor/tests/end-to-end/api/09-rooms.js @@ -7,7 +7,7 @@ import { after, afterEach, before, beforeEach, describe, it } from 'mocha'; import { sleep } from '../../../lib/utils/sleep'; import { getCredentials, api, request, credentials } from '../../data/api-data.js'; import { sendSimpleMessage, deleteMessage } from '../../data/chat.helper'; -import { imgURL } from '../../data/interactions.js'; +import { imgURL } from '../../data/interactions'; import { updateEEPermission, updatePermission, updateSetting } from '../../data/permissions.helper'; import { closeRoom, createRoom } from '../../data/rooms.helper'; import { password } from '../../data/user'; diff --git a/apps/meteor/tests/end-to-end/api/14-assets.js b/apps/meteor/tests/end-to-end/api/14-assets.js index 4e9c61b53301..8248e8c04f09 100644 --- a/apps/meteor/tests/end-to-end/api/14-assets.js +++ b/apps/meteor/tests/end-to-end/api/14-assets.js @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { before, describe, it } from 'mocha'; import { getCredentials, api, request, credentials } from '../../data/api-data.js'; -import { imgURL } from '../../data/interactions.js'; +import { imgURL } from '../../data/interactions'; describe('[Assets]', function () { this.retries(0); From fff548fe8aa5d438f90596c778f0cc5906dc7981 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Wed, 18 Oct 2023 17:03:20 -0300 Subject: [PATCH 26/38] feat: add trial flag to licenses.info endpoint (#30662) --- ee/packages/license/src/definition/LicenseInfo.ts | 1 + ee/packages/license/src/license.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/ee/packages/license/src/definition/LicenseInfo.ts b/ee/packages/license/src/definition/LicenseInfo.ts index 7de3c0cfbdd6..4c4e34d30528 100644 --- a/ee/packages/license/src/definition/LicenseInfo.ts +++ b/ee/packages/license/src/definition/LicenseInfo.ts @@ -7,4 +7,5 @@ export type LicenseInfo = { activeModules: LicenseModule[]; limits: Record; tags: ILicenseTag[]; + trial: boolean; }; diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index d24d91287d1e..14dceedd735a 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -332,6 +332,7 @@ export class LicenseManager extends Emitter { activeModules, limits: limits as Record, tags: license?.information.tags || [], + trial: Boolean(license?.information.trial), }; } } From 7b02ca3b4dc537d8abeed0f3d633a3e14fd0d2d5 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Thu, 19 Oct 2023 00:09:45 -0300 Subject: [PATCH 27/38] refactor(uikit): uikit interactions (#30534) --- .../app/apps/server/bridges/uiInteraction.ts | 7 +- .../app/ui-message/client/ActionManager.js | 261 --------------- .../app/ui-message/client/ActionManager.ts | 237 ++++++++++++++ .../client/UiKitTriggerTimeoutError.ts | 7 + apps/meteor/app/ui/client/lib/ChatMessages.ts | 4 +- .../UIKit/hooks/useUIKitHandleAction.tsx | 26 -- .../UIKit/hooks/useUIKitHandleClose.tsx | 34 -- .../UIKit/hooks/useUIKitStateManager.tsx | 36 --- .../hooks/useUiKitActionManager.ts | 0 .../meteor/client/UIKit/hooks/useUiKitView.ts | 93 ++++++ .../components/ActionManagerBusyState.tsx | 8 +- .../message/uikit/UiKitMessageBlock.tsx | 86 +++-- .../variants/room/RoomMessageContent.tsx | 2 +- .../variants/thread/ThreadMessageContent.tsx | 2 +- .../client/hooks/useAppActionButtons.ts | 105 ++++-- .../client/hooks/useAppUiKitInteraction.ts | 19 +- apps/meteor/client/lib/banners.ts | 6 +- .../lib/chats/flows/processSlashCommand.ts | 4 +- .../client/lib/utils/preventSyntheticEvent.ts | 9 + apps/meteor/client/polyfills/index.ts | 1 + .../meteor/client/polyfills/promiseFinally.ts | 16 + .../providers/ActionManagerProvider.tsx | 6 +- .../moderation/helpers/ContextMessage.tsx | 2 +- .../client/views/banners/BannerRegion.tsx | 2 +- .../client/views/banners/UiKitBanner.tsx | 90 ++++-- .../views/banners/hooks/useRemoteBanners.ts | 4 +- .../client/views/modal/uikit/ModalBlock.tsx | 10 +- .../client/views/modal/uikit/UiKitModal.tsx | 195 ++++++----- .../views/modal/uikit/getButtonStyle.ts | 6 +- .../uikit/hooks/useActionManagerState.ts | 39 --- .../views/modal/uikit/hooks/useValues.ts | 48 --- .../MessageList/ContactHistoryMessage.tsx | 2 +- apps/meteor/client/views/room/Room.tsx | 11 +- .../uikit/UiKitContextualBar.tsx | 306 ++++++------------ .../views/room/hooks/useAppsContextualBar.ts | 64 ++-- .../providers/hooks/useAppsRoomActions.ts | 36 ++- .../ee/app/license/server/maxSeatsBanners.ts | 12 +- .../ee/client/apps/gameCenter/GameCenter.tsx | 15 +- .../ee/server/apps/communication/uikit.ts | 120 +++---- .../server/modules/core-apps/banner.module.ts | 18 +- .../server/modules/core-apps/nps.module.ts | 26 +- .../modules/core-apps/videoconf.module.ts | 10 +- .../services/nps/getAndCreateNpsSurvey.ts | 4 +- .../server/services/nps/notification.ts | 6 +- apps/meteor/server/services/startup.ts | 4 +- .../server/services/uikit-core-app/service.ts | 14 +- .../services/video-conference/service.ts | 8 +- ee/packages/ddp-client/src/types/streams.ts | 4 +- package.json | 2 +- packages/core-services/src/Events.ts | 4 +- packages/core-services/src/index.ts | 3 +- .../core-services/src/types/INPSService.ts | 4 +- .../core-services/src/types/IUiKitCoreApp.ts | 51 ++- .../src/types/IVideoConfService.ts | 4 +- packages/core-typings/src/IBanner.ts | 4 +- packages/core-typings/src/INps.ts | 2 +- packages/core-typings/src/Serialized.ts | 31 +- packages/core-typings/src/UIKit.ts | 60 ---- .../core-typings/src/cloud/Announcement.ts | 4 +- packages/core-typings/src/index.ts | 3 +- packages/core-typings/src/uikit/BannerView.ts | 16 + .../src/uikit/ContextualBarView.ts | 14 + packages/core-typings/src/uikit/ModalView.ts | 15 + .../src/uikit/ServerInteraction.ts | 84 +++++ .../core-typings/src/uikit/UserInteraction.ts | 122 +++++++ packages/core-typings/src/uikit/View.ts | 9 + packages/core-typings/src/uikit/index.ts | 17 + packages/core-typings/src/utils.ts | 2 + .../src/contexts/UiKitContext.ts | 11 +- .../src/elements/MarkdownTextElement.tsx | 5 +- .../src/elements/PlainTextElement.tsx | 5 +- .../src/extractInitialStateFromLayout.ts | 90 ++++++ .../src/hooks/useUiKitContext.ts | 5 - .../src/hooks/useUiKitState.ts | 81 +++-- .../src/hooks/useUiKitStateValue.ts | 18 -- packages/fuselage-ui-kit/src/index.ts | 1 + .../src/MockedAppRootBuilder.tsx | 11 +- packages/rest-typings/src/apps/index.ts | 12 +- .../ui-contexts/src/ActionManagerContext.ts | 57 +--- yarn.lock | 58 ++-- 80 files changed, 1531 insertions(+), 1299 deletions(-) delete mode 100644 apps/meteor/app/ui-message/client/ActionManager.js create mode 100644 apps/meteor/app/ui-message/client/ActionManager.ts create mode 100644 apps/meteor/app/ui-message/client/UiKitTriggerTimeoutError.ts delete mode 100644 apps/meteor/client/UIKit/hooks/useUIKitHandleAction.tsx delete mode 100644 apps/meteor/client/UIKit/hooks/useUIKitHandleClose.tsx delete mode 100644 apps/meteor/client/UIKit/hooks/useUIKitStateManager.tsx rename apps/meteor/client/{ => UIKit}/hooks/useUiKitActionManager.ts (100%) create mode 100644 apps/meteor/client/UIKit/hooks/useUiKitView.ts create mode 100644 apps/meteor/client/lib/utils/preventSyntheticEvent.ts create mode 100644 apps/meteor/client/polyfills/promiseFinally.ts delete mode 100644 apps/meteor/client/views/modal/uikit/hooks/useActionManagerState.ts delete mode 100644 apps/meteor/client/views/modal/uikit/hooks/useValues.ts delete mode 100644 packages/core-typings/src/UIKit.ts create mode 100644 packages/core-typings/src/uikit/BannerView.ts create mode 100644 packages/core-typings/src/uikit/ContextualBarView.ts create mode 100644 packages/core-typings/src/uikit/ModalView.ts create mode 100644 packages/core-typings/src/uikit/ServerInteraction.ts create mode 100644 packages/core-typings/src/uikit/UserInteraction.ts create mode 100644 packages/core-typings/src/uikit/View.ts create mode 100644 packages/core-typings/src/uikit/index.ts create mode 100644 packages/fuselage-ui-kit/src/extractInitialStateFromLayout.ts delete mode 100644 packages/fuselage-ui-kit/src/hooks/useUiKitContext.ts delete mode 100644 packages/fuselage-ui-kit/src/hooks/useUiKitStateValue.ts diff --git a/apps/meteor/app/apps/server/bridges/uiInteraction.ts b/apps/meteor/app/apps/server/bridges/uiInteraction.ts index b51c3be8ae3b..8e94f66e9617 100644 --- a/apps/meteor/app/apps/server/bridges/uiInteraction.ts +++ b/apps/meteor/app/apps/server/bridges/uiInteraction.ts @@ -1,11 +1,12 @@ import type { IUIKitInteraction } from '@rocket.chat/apps-engine/definition/uikit'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; -import { UiInteractionBridge as UiIntBridge } from '@rocket.chat/apps-engine/server/bridges/UiInteractionBridge'; +import { UiInteractionBridge as AppsEngineUiInteractionBridge } from '@rocket.chat/apps-engine/server/bridges/UiInteractionBridge'; import { api } from '@rocket.chat/core-services'; +import type { UiKit } from '@rocket.chat/core-typings'; import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; -export class UiInteractionBridge extends UiIntBridge { +export class UiInteractionBridge extends AppsEngineUiInteractionBridge { constructor(private readonly orch: AppServerOrchestrator) { super(); } @@ -19,6 +20,6 @@ export class UiInteractionBridge extends UiIntBridge { throw new Error('Invalid app provided'); } - void api.broadcast('notify.uiInteraction', user.id, interaction); + void api.broadcast('notify.uiInteraction', user.id, interaction as UiKit.ServerInteraction); } } diff --git a/apps/meteor/app/ui-message/client/ActionManager.js b/apps/meteor/app/ui-message/client/ActionManager.js deleted file mode 100644 index ebe9d1aed093..000000000000 --- a/apps/meteor/app/ui-message/client/ActionManager.js +++ /dev/null @@ -1,261 +0,0 @@ -import { UIKitIncomingInteractionType } from '@rocket.chat/apps-engine/definition/uikit'; -import { UIKitInteractionTypes } from '@rocket.chat/core-typings'; -import { Emitter } from '@rocket.chat/emitter'; -import { Random } from '@rocket.chat/random'; -import { lazy } from 'react'; - -import * as banners from '../../../client/lib/banners'; -import { imperativeModal } from '../../../client/lib/imperativeModal'; -import { dispatchToastMessage } from '../../../client/lib/toast'; -import { router } from '../../../client/providers/RouterProvider'; -import { sdk } from '../../utils/client/lib/SDKClient'; -import { t } from '../../utils/lib/i18n'; - -const UiKitModal = lazy(() => import('../../../client/views/modal/uikit/UiKitModal')); - -export const events = new Emitter(); - -export const on = (...args) => { - events.on(...args); -}; - -export const off = (...args) => { - events.off(...args); -}; - -const TRIGGER_TIMEOUT = 5000; - -const TRIGGER_TIMEOUT_ERROR = 'TRIGGER_TIMEOUT_ERROR'; - -const triggersId = new Map(); - -const instances = new Map(); - -const invalidateTriggerId = (id) => { - const appId = triggersId.get(id); - triggersId.delete(id); - return appId; -}; - -export const generateTriggerId = (appId) => { - const triggerId = Random.id(); - triggersId.set(triggerId, appId); - setTimeout(invalidateTriggerId, TRIGGER_TIMEOUT, triggerId); - return triggerId; -}; - -export const handlePayloadUserInteraction = (type, { /* appId,*/ triggerId, ...data }) => { - if (!triggersId.has(triggerId)) { - return; - } - const appId = invalidateTriggerId(triggerId); - if (!appId) { - return; - } - - const { view } = data; - let { viewId } = data; - - if (view && view.id) { - viewId = view.id; - } - - if (!viewId) { - return; - } - - if ([UIKitInteractionTypes.ERRORS].includes(type)) { - events.emit(viewId, { - type, - triggerId, - viewId, - appId, - ...data, - }); - return UIKitInteractionTypes.ERRORS; - } - - if ( - [UIKitInteractionTypes.BANNER_UPDATE, UIKitInteractionTypes.MODAL_UPDATE, UIKitInteractionTypes.CONTEXTUAL_BAR_UPDATE].includes(type) - ) { - events.emit(viewId, { - type, - triggerId, - viewId, - appId, - ...data, - }); - return type; - } - - if ([UIKitInteractionTypes.MODAL_OPEN].includes(type)) { - const instance = imperativeModal.open({ - component: UiKitModal, - props: { - triggerId, - viewId, - appId, - ...data, - }, - }); - - instances.set(viewId, { - close() { - instance.close(); - instances.delete(viewId); - }, - }); - - return UIKitInteractionTypes.MODAL_OPEN; - } - - if ([UIKitInteractionTypes.CONTEXTUAL_BAR_OPEN].includes(type)) { - instances.set(viewId, { - payload: { - type, - triggerId, - appId, - viewId, - ...data, - }, - close() { - instances.delete(viewId); - }, - }); - - router.navigate({ - name: router.getRouteName(), - params: { - ...router.getRouteParameters(), - tab: 'app', - context: viewId, - }, - }); - - return UIKitInteractionTypes.CONTEXTUAL_BAR_OPEN; - } - - if ([UIKitInteractionTypes.BANNER_OPEN].includes(type)) { - banners.open(data); - instances.set(viewId, { - close() { - banners.closeById(viewId); - }, - }); - return UIKitInteractionTypes.BANNER_OPEN; - } - - if ([UIKitIncomingInteractionType.BANNER_CLOSE].includes(type)) { - const instance = instances.get(viewId); - - if (instance) { - instance.close(); - } - return UIKitIncomingInteractionType.BANNER_CLOSE; - } - - if ([UIKitIncomingInteractionType.CONTEXTUAL_BAR_CLOSE].includes(type)) { - const instance = instances.get(viewId); - - if (instance) { - instance.close(); - } - return UIKitIncomingInteractionType.CONTEXTUAL_BAR_CLOSE; - } - - return UIKitInteractionTypes.MODAL_ClOSE; -}; - -export const triggerAction = async ({ type, actionId, appId, rid, mid, viewId, container, tmid, ...rest }) => - new Promise(async (resolve, reject) => { - events.emit('busy', { busy: true }); - - const triggerId = generateTriggerId(appId); - - const payload = rest.payload || rest; - - setTimeout(reject, TRIGGER_TIMEOUT, [TRIGGER_TIMEOUT_ERROR, { triggerId, appId }]); - - const { type: interactionType, ...data } = await (async () => { - try { - return await sdk.rest.post(`/apps/ui.interaction/${appId}`, { - type, - actionId, - payload, - container, - mid, - rid, - tmid, - triggerId, - viewId, - }); - } catch (e) { - reject(e); - return {}; - } finally { - events.emit('busy', { busy: false }); - } - })(); - - return resolve(handlePayloadUserInteraction(interactionType, data)); - }); - -export const triggerBlockAction = (options) => triggerAction({ type: UIKitIncomingInteractionType.BLOCK, ...options }); - -export const triggerActionButtonAction = (options) => - triggerAction({ type: UIKitIncomingInteractionType.ACTION_BUTTON, ...options }).catch(async (reason) => { - if (Array.isArray(reason) && reason[0] === TRIGGER_TIMEOUT_ERROR) { - dispatchToastMessage({ - type: 'error', - message: t('UIKit_Interaction_Timeout'), - }); - } - }); - -export const triggerSubmitView = async ({ viewId, ...options }) => { - const close = () => { - const instance = instances.get(viewId); - - if (instance) { - instance.close(); - } - }; - - try { - const result = await triggerAction({ - type: UIKitIncomingInteractionType.VIEW_SUBMIT, - viewId, - ...options, - }); - if (!result || UIKitInteractionTypes.MODAL_CLOSE === result) { - close(); - } - } catch { - close(); - } -}; - -export const triggerCancel = async ({ view, ...options }) => { - const instance = instances.get(view.id); - try { - await triggerAction({ type: UIKitIncomingInteractionType.VIEW_CLOSED, view, ...options }); - } finally { - if (instance) { - instance.close(); - } - } -}; - -export const getUserInteractionPayloadByViewId = (viewId) => { - if (!viewId) { - throw new Error('No viewId provided when checking for `user interaction payload`'); - } - - const instance = instances.get(viewId); - - if (!instance) { - return {}; - } - - return instance.payload; -}; diff --git a/apps/meteor/app/ui-message/client/ActionManager.ts b/apps/meteor/app/ui-message/client/ActionManager.ts new file mode 100644 index 000000000000..14650c3e12a0 --- /dev/null +++ b/apps/meteor/app/ui-message/client/ActionManager.ts @@ -0,0 +1,237 @@ +import type { DistributiveOmit, UiKit } from '@rocket.chat/core-typings'; +import { Emitter } from '@rocket.chat/emitter'; +import { Random } from '@rocket.chat/random'; +import type { ActionManagerContext, RouterContext } from '@rocket.chat/ui-contexts'; +import type { ContextType } from 'react'; +import { lazy } from 'react'; + +import * as banners from '../../../client/lib/banners'; +import { imperativeModal } from '../../../client/lib/imperativeModal'; +import { router } from '../../../client/providers/RouterProvider'; +import { sdk } from '../../utils/client/lib/SDKClient'; +import { UiKitTriggerTimeoutError } from './UiKitTriggerTimeoutError'; + +const UiKitModal = lazy(() => import('../../../client/views/modal/uikit/UiKitModal')); + +type ActionManagerType = Exclude, undefined>; + +export class ActionManager implements ActionManagerType { + protected static TRIGGER_TIMEOUT = 5000; + + protected static TRIGGER_TIMEOUT_ERROR = 'TRIGGER_TIMEOUT_ERROR'; + + protected events = new Emitter<{ busy: { busy: boolean }; [viewId: string]: any }>(); + + protected triggersId = new Map(); + + protected viewInstances = new Map< + string, + { + payload?: { + view: UiKit.ContextualBarView; + }; + close: () => void; + } + >(); + + public constructor(protected router: ContextType) {} + + protected invalidateTriggerId(id: string) { + const appId = this.triggersId.get(id); + this.triggersId.delete(id); + return appId; + } + + public on(viewId: string, listener: (data: any) => void): void; + + public on(eventName: 'busy', listener: ({ busy }: { busy: boolean }) => void): void; + + public on(eventName: string, listener: (data: any) => void) { + return this.events.on(eventName, listener); + } + + public off(viewId: string, listener: (data: any) => any): void; + + public off(eventName: 'busy', listener: ({ busy }: { busy: boolean }) => void): void; + + public off(eventName: string, listener: (data: any) => void) { + return this.events.off(eventName, listener); + } + + public generateTriggerId(appId: string | undefined) { + const triggerId = Random.id(); + this.triggersId.set(triggerId, appId); + setTimeout(() => this.invalidateTriggerId(triggerId), ActionManager.TRIGGER_TIMEOUT); + return triggerId; + } + + public async emitInteraction(appId: string, userInteraction: DistributiveOmit) { + this.events.emit('busy', { busy: true }); + + const triggerId = this.generateTriggerId(appId); + + let timeout: ReturnType | undefined; + + await Promise.race([ + new Promise((_, reject) => { + timeout = setTimeout(() => reject(new UiKitTriggerTimeoutError('Timeout', { triggerId, appId })), ActionManager.TRIGGER_TIMEOUT); + }), + sdk.rest + .post(`/apps/ui.interaction/${appId}`, { + ...userInteraction, + triggerId, + }) + .then((interaction) => this.handleServerInteraction(interaction)), + ]).finally(() => { + if (timeout) clearTimeout(timeout); + this.events.emit('busy', { busy: false }); + }); + } + + public handleServerInteraction(interaction: UiKit.ServerInteraction) { + const { triggerId } = interaction; + + if (!this.triggersId.has(triggerId)) { + return; + } + + const appId = this.invalidateTriggerId(triggerId); + if (!appId) { + return; + } + + switch (interaction.type) { + case 'errors': { + const { type, triggerId, viewId, appId, errors } = interaction; + this.events.emit(interaction.viewId, { + type, + triggerId, + viewId, + appId, + errors, + }); + break; + } + + case 'modal.open': { + const { view } = interaction; + const instance = imperativeModal.open({ + component: UiKitModal, + props: { + key: view.id, + initialView: interaction.view, + }, + }); + + this.viewInstances.set(view.id, { + close: () => { + instance.close(); + this.viewInstances.delete(view.id); + }, + }); + break; + } + + case 'modal.update': + case 'contextual_bar.update': { + const { type, triggerId, appId, view } = interaction; + this.events.emit(view.id, { + type, + triggerId, + viewId: view.id, + appId, + view, + }); + break; + } + + case 'modal.close': { + break; + } + + case 'banner.open': { + const { type, triggerId, ...view } = interaction; + banners.open(view); + this.viewInstances.set(view.viewId, { + close: () => { + banners.closeById(view.viewId); + }, + }); + break; + } + + case 'banner.update': { + const { type, triggerId, appId, view } = interaction; + this.events.emit(view.viewId, { + type, + triggerId, + viewId: view.viewId, + appId, + view, + }); + break; + } + + case 'banner.close': { + const { viewId } = interaction; + this.viewInstances.get(viewId)?.close(); + + break; + } + + case 'contextual_bar.open': { + const { view } = interaction; + this.viewInstances.set(view.id, { + payload: { + view, + }, + close: () => { + this.viewInstances.delete(view.id); + }, + }); + + const routeName = this.router.getRouteName(); + const routeParams = this.router.getRouteParameters(); + + if (!routeName) { + break; + } + + this.router.navigate({ + name: routeName, + params: { + ...routeParams, + tab: 'app', + context: view.id, + }, + }); + break; + } + + case 'contextual_bar.close': { + const { view } = interaction; + this.viewInstances.get(view.id)?.close(); + break; + } + } + + return interaction.type; + } + + public getInteractionPayloadByViewId(viewId: UiKit.ContextualBarView['id']) { + if (!viewId) { + throw new Error('No viewId provided when checking for `user interaction payload`'); + } + + return this.viewInstances.get(viewId)?.payload; + } + + public disposeView(viewId: UiKit.ModalView['id'] | UiKit.BannerView['viewId'] | UiKit.ContextualBarView['id']) { + const instance = this.viewInstances.get(viewId); + instance?.close?.(); + this.viewInstances.delete(viewId); + } +} + +/** @deprecated consumer should use the context instead */ +export const actionManager = new ActionManager(router); diff --git a/apps/meteor/app/ui-message/client/UiKitTriggerTimeoutError.ts b/apps/meteor/app/ui-message/client/UiKitTriggerTimeoutError.ts new file mode 100644 index 000000000000..75b035d822a1 --- /dev/null +++ b/apps/meteor/app/ui-message/client/UiKitTriggerTimeoutError.ts @@ -0,0 +1,7 @@ +import { RocketChatError } from '../../../client/lib/errors/RocketChatError'; + +export class UiKitTriggerTimeoutError extends RocketChatError<'trigger-timeout'> { + constructor(message = 'Timeout', details: { triggerId: string; appId: string }) { + super('trigger-timeout', message, details); + } +} diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index 4a4b04f11833..4563bae81d52 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -18,7 +18,7 @@ import { setHighlightMessage, clearHighlightMessage, } from '../../../../client/views/room/MessageList/providers/messageHighlightSubscription'; -import * as ActionManager from '../../../ui-message/client/ActionManager'; +import { actionManager } from '../../../ui-message/client/ActionManager'; import { UserAction } from './UserAction'; type DeepWritable = T extends (...args: any) => any @@ -150,7 +150,7 @@ export class ChatMessages implements ChatAPI { this.uid = params.uid; this.data = createDataAPI({ rid, tmid }); this.uploads = createUploadsAPI({ rid, tmid }); - this.ActionManager = ActionManager; + this.ActionManager = actionManager; const unimplemented = () => { throw new Error('Flow is not implemented'); diff --git a/apps/meteor/client/UIKit/hooks/useUIKitHandleAction.tsx b/apps/meteor/client/UIKit/hooks/useUIKitHandleAction.tsx deleted file mode 100644 index 6a97f18a7936..000000000000 --- a/apps/meteor/client/UIKit/hooks/useUIKitHandleAction.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { UIKitIncomingInteractionContainerType } from '@rocket.chat/apps-engine/definition/uikit/UIKitIncomingInteractionContainer'; -import type { UiKitPayload, UIKitActionEvent } from '@rocket.chat/core-typings'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; - -import { useUiKitActionManager } from '../../hooks/useUiKitActionManager'; - -const useUIKitHandleAction = (state: S): ((event: UIKitActionEvent) => Promise) => { - const actionManager = useUiKitActionManager(); - return useMutableCallback(async ({ blockId, value, appId, actionId }) => { - if (!appId) { - throw new Error('useUIKitHandleAction - invalid appId'); - } - return actionManager.triggerBlockAction({ - container: { - type: UIKitIncomingInteractionContainerType.VIEW, - id: state.viewId || state.appId, - }, - actionId, - appId, - value, - blockId, - }); - }); -}; - -export { useUIKitHandleAction }; diff --git a/apps/meteor/client/UIKit/hooks/useUIKitHandleClose.tsx b/apps/meteor/client/UIKit/hooks/useUIKitHandleClose.tsx deleted file mode 100644 index 672e1b311b5d..000000000000 --- a/apps/meteor/client/UIKit/hooks/useUIKitHandleClose.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type { UIKitInteractionType } from '@rocket.chat/apps-engine/definition/uikit'; -import type { UiKitPayload } from '@rocket.chat/core-typings'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; - -import { useUiKitActionManager } from '../../hooks/useUiKitActionManager'; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const emptyFn = (_error: any, _result: UIKitInteractionType | void): void => undefined; - -const useUIKitHandleClose = (state: S, fn = emptyFn): (() => Promise) => { - const actionManager = useUiKitActionManager(); - const dispatchToastMessage = useToastMessageDispatch(); - return useMutableCallback(() => - actionManager - .triggerCancel({ - appId: state.appId, - viewId: state.viewId, - view: { - ...state, - id: state.viewId, - }, - isCleared: true, - }) - .then((result) => fn(undefined, result)) - .catch((error) => { - dispatchToastMessage({ type: 'error', message: error }); - fn(error, undefined); - return Promise.reject(error); - }), - ); -}; - -export { useUIKitHandleClose }; diff --git a/apps/meteor/client/UIKit/hooks/useUIKitStateManager.tsx b/apps/meteor/client/UIKit/hooks/useUIKitStateManager.tsx deleted file mode 100644 index 26b329f2ea60..000000000000 --- a/apps/meteor/client/UIKit/hooks/useUIKitStateManager.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { UIKitUserInteractionResult, UiKitPayload } from '@rocket.chat/core-typings'; -import { isErrorType } from '@rocket.chat/core-typings'; -import { useSafely } from '@rocket.chat/fuselage-hooks'; -import { useEffect, useState } from 'react'; - -import { useUiKitActionManager } from '../../hooks/useUiKitActionManager'; - -const useUIKitStateManager = (initialState: S): S => { - const actionManager = useUiKitActionManager(); - const [state, setState] = useSafely(useState(initialState)); - - const { viewId } = state; - - useEffect(() => { - const handleUpdate = ({ ...data }: UIKitUserInteractionResult): void => { - if (isErrorType(data)) { - const { errors } = data; - setState((state) => ({ ...state, errors })); - return; - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { type, ...rest } = data; - setState(rest as any); - }; - - actionManager.on(viewId, handleUpdate); - - return (): void => { - actionManager.off(viewId, handleUpdate); - }; - }, [setState, viewId]); - - return state; -}; - -export { useUIKitStateManager }; diff --git a/apps/meteor/client/hooks/useUiKitActionManager.ts b/apps/meteor/client/UIKit/hooks/useUiKitActionManager.ts similarity index 100% rename from apps/meteor/client/hooks/useUiKitActionManager.ts rename to apps/meteor/client/UIKit/hooks/useUiKitActionManager.ts diff --git a/apps/meteor/client/UIKit/hooks/useUiKitView.ts b/apps/meteor/client/UIKit/hooks/useUiKitView.ts new file mode 100644 index 000000000000..2d0d1512bc17 --- /dev/null +++ b/apps/meteor/client/UIKit/hooks/useUiKitView.ts @@ -0,0 +1,93 @@ +import type { UiKit } from '@rocket.chat/core-typings'; +import { useSafely } from '@rocket.chat/fuselage-hooks'; +import { extractInitialStateFromLayout } from '@rocket.chat/fuselage-ui-kit'; +import type { Dispatch } from 'react'; +import { useEffect, useMemo, useReducer, useState } from 'react'; + +import { useUiKitActionManager } from './useUiKitActionManager'; + +const reduceValues = ( + values: { [actionId: string]: { value: unknown; blockId?: string } }, + { actionId, payload }: { actionId: string; payload: { value: unknown; blockId?: string } }, +): { [actionId: string]: { value: unknown; blockId?: string } } => ({ + ...values, + [actionId]: payload, +}); + +const getViewId = (view: UiKit.View): string => { + if ('id' in view && typeof view.id === 'string') { + return view.id; + } + + if ('viewId' in view && typeof view.viewId === 'string') { + return view.viewId; + } + + throw new Error('Invalid view'); +}; + +const getViewFromInteraction = (interaction: UiKit.ServerInteraction): UiKit.View | undefined => { + if ('view' in interaction && typeof interaction.view === 'object') { + return interaction.view; + } + + if (interaction.type === 'banner.open') { + return interaction; + } + + return undefined; +}; + +type UseUiKitViewReturnType = { + view: TView; + errors?: { [field: string]: string }[]; + values: { [actionId: string]: { value: unknown; blockId?: string } }; + updateValues: Dispatch<{ actionId: string; payload: { value: unknown; blockId?: string } }>; + state: { + [blockId: string]: { + [key: string]: unknown; + }; + }; +}; + +export function useUiKitView(initialView: S): UseUiKitViewReturnType { + const [errors, setErrors] = useSafely(useState<{ [field: string]: string }[] | undefined>()); + const [values, updateValues] = useSafely(useReducer(reduceValues, initialView.blocks, extractInitialStateFromLayout)); + const [view, updateView] = useSafely(useState(initialView)); + const actionManager = useUiKitActionManager(); + + const state = useMemo(() => { + return Object.entries(values).reduce<{ [blockId: string]: { [actionId: string]: unknown } }>((obj, [key, payload]) => { + if (!payload?.blockId) { + return obj; + } + + const { blockId, value } = payload; + obj[blockId] = obj[blockId] || {}; + obj[blockId][key] = value; + + return obj; + }, {}); + }, [values]); + + const viewId = getViewId(view); + + useEffect(() => { + const handleUpdate = (interaction: UiKit.ServerInteraction): void => { + if (interaction.type === 'errors') { + setErrors(interaction.errors); + return; + } + + updateView((view) => ({ ...view, ...getViewFromInteraction(interaction) })); + }; + + actionManager.on(viewId, handleUpdate); + + return (): void => { + actionManager.off(viewId, handleUpdate); + }; + }, [actionManager, setErrors, updateView, viewId]); + + return { view, errors, values, updateValues, state }; +} diff --git a/apps/meteor/client/components/ActionManagerBusyState.tsx b/apps/meteor/client/components/ActionManagerBusyState.tsx index 0374254a7de9..033b200a2aa7 100644 --- a/apps/meteor/client/components/ActionManagerBusyState.tsx +++ b/apps/meteor/client/components/ActionManagerBusyState.tsx @@ -3,7 +3,7 @@ import { Box } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useEffect, useState } from 'react'; -import { useUiKitActionManager } from '../hooks/useUiKitActionManager'; +import { useUiKitActionManager } from '../UIKit/hooks/useUiKitActionManager'; const ActionManagerBusyState = () => { const t = useTranslation(); @@ -15,10 +15,12 @@ const ActionManagerBusyState = () => { return; } - actionManager.on('busy', ({ busy }: { busy: boolean }) => setBusy(busy)); + const handleBusyStateChange = ({ busy }: { busy: boolean }) => setBusy(busy); + + actionManager.on('busy', handleBusyStateChange); return () => { - actionManager.off('busy'); + actionManager.off('busy', handleBusyStateChange); }; }, [actionManager]); diff --git a/apps/meteor/client/components/message/uikit/UiKitMessageBlock.tsx b/apps/meteor/client/components/message/uikit/UiKitMessageBlock.tsx index d59314ae8198..6d86e724b95f 100644 --- a/apps/meteor/client/components/message/uikit/UiKitMessageBlock.tsx +++ b/apps/meteor/client/components/message/uikit/UiKitMessageBlock.tsx @@ -1,12 +1,12 @@ -import { UIKitIncomingInteractionContainerType } from '@rocket.chat/apps-engine/definition/uikit/UIKitIncomingInteractionContainer'; import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import { MessageBlock } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { UiKitComponent, UiKitMessage as UiKitMessageSurfaceRender, UiKitContext } from '@rocket.chat/fuselage-ui-kit'; import type { MessageSurfaceLayout } from '@rocket.chat/ui-kit'; import type { ContextType, ReactElement } from 'react'; -import React from 'react'; +import React, { useMemo } from 'react'; +import { useUiKitActionManager } from '../../../UIKit/hooks/useUiKitActionManager'; import { useVideoConfDispatchOutgoing, useVideoConfIsCalling, @@ -15,27 +15,16 @@ import { useVideoConfManager, useVideoConfSetPreferences, } from '../../../contexts/VideoConfContext'; -import { useUiKitActionManager } from '../../../hooks/useUiKitActionManager'; import { useVideoConfWarning } from '../../../views/room/contextualBar/VideoConference/hooks/useVideoConfWarning'; import GazzodownText from '../../GazzodownText'; -let patched = false; -const patchMessageParser = () => { - if (patched) { - return; - } - - patched = true; -}; - type UiKitMessageBlockProps = { + rid: IRoom['_id']; mid: IMessage['_id']; blocks: MessageSurfaceLayout; - rid: IRoom['_id']; - appId?: string | boolean; // TODO: this is a hack while the context value is not properly typed }; -const UiKitMessageBlock = ({ mid: _mid, blocks, rid, appId }: UiKitMessageBlockProps): ReactElement => { +const UiKitMessageBlock = ({ rid, mid, blocks }: UiKitMessageBlockProps): ReactElement => { const joinCall = useVideoConfJoinCall(); const setPreferences = useVideoConfSetPreferences(); const isCalling = useVideoConfIsCalling(); @@ -61,44 +50,47 @@ const UiKitMessageBlock = ({ mid: _mid, blocks, rid, appId }: UiKitMessageBlockP const actionManager = useUiKitActionManager(); // TODO: this structure is attrociously wrong; we should revisit this - const context: ContextType = { - // @ts-ignore Property 'mid' does not exist on type 'ActionParams'. - action: ({ actionId, value, blockId, mid = _mid, appId }, event) => { - if (appId === 'videoconf-core') { - event.preventDefault(); - setPreferences({ mic: true, cam: false }); - if (actionId === 'join') { - return joinCall(blockId); - } + const contextValue = useMemo( + (): ContextType => ({ + action: ({ appId, actionId, blockId, value }, event) => { + if (appId === 'videoconf-core') { + event.preventDefault(); + setPreferences({ mic: true, cam: false }); + if (actionId === 'join') { + return joinCall(blockId); + } - if (actionId === 'callBack') { - return handleOpenVideoConf(blockId); + if (actionId === 'callBack') { + return handleOpenVideoConf(blockId); + } } - } - actionManager?.triggerBlockAction({ - blockId, - actionId, - value, - mid, - rid, - appId, - container: { - type: UIKitIncomingInteractionContainerType.MESSAGE, - id: mid, - }, - }); - }, - // @ts-ignore Type 'string | boolean | undefined' is not assignable to type 'string'. - appId, - rid, - }; - - patchMessageParser(); // TODO: this is a hack + actionManager.emitInteraction(appId, { + type: 'blockAction', + actionId, + payload: { + blockId, + value, + }, + container: { + type: 'message', + id: mid, + }, + rid, + mid, + }); + }, + appId: '', // TODO: this is a hack + rid, + state: () => undefined, // TODO: this is a hack + values: {}, // TODO: this is a hack + }), + [actionManager, handleOpenVideoConf, joinCall, mid, rid, setPreferences], + ); return ( - + diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 2b54588c6263..b22627bea8d2 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -61,7 +61,7 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM )} {normalizedMessage.blocks && ( - + )} {!!normalizedMessage?.attachments?.length && } diff --git a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx index 655f96639929..57835ec75e0c 100644 --- a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx @@ -49,7 +49,7 @@ const ThreadMessageContent = ({ message }: ThreadMessageContentProps): ReactElem )} {normalizedMessage.blocks && ( - + )} {normalizedMessage.attachments && } diff --git a/apps/meteor/client/hooks/useAppActionButtons.ts b/apps/meteor/client/hooks/useAppActionButtons.ts index 28d62ef1b75a..d039b2bd7c71 100644 --- a/apps/meteor/client/hooks/useAppActionButtons.ts +++ b/apps/meteor/client/hooks/useAppActionButtons.ts @@ -1,20 +1,22 @@ import type { IUIActionButton, UIActionButtonContext } from '@rocket.chat/apps-engine/definition/ui'; import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks'; -import { useEndpoint, useSingleStream, useUserId } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useSingleStream, useToastMessageDispatch, useUserId } from '@rocket.chat/ui-contexts'; import type { UseQueryResult } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { UiKitTriggerTimeoutError } from '../../app/ui-message/client/UiKitTriggerTimeoutError'; import type { MessageActionConfig, MessageActionContext } from '../../app/ui-utils/client/lib/MessageAction'; import type { MessageBoxAction } from '../../app/ui-utils/client/lib/messageBox'; import { Utilities } from '../../ee/lib/misc/Utilities'; +import { useUiKitActionManager } from '../UIKit/hooks/useUiKitActionManager'; import type { GenericMenuItemProps } from '../components/GenericMenu/GenericMenuItem'; import { useApplyButtonFilters, useApplyButtonAuthFilter } from './useApplyButtonFilters'; -import { useUiKitActionManager } from './useUiKitActionManager'; const getIdForActionButton = ({ appId, actionId }: IUIActionButton): string => `${appId}/${actionId}`; -export const useAppActionButtons = (context?: `${UIActionButtonContext}`) => { +export const useAppActionButtons = (context?: TContext) => { const queryClient = useQueryClient(); const apps = useSingleStream('apps'); @@ -24,7 +26,14 @@ export const useAppActionButtons = (context?: `${UIActionButtonContext}`) => { const result = useQuery(['apps', 'actionButtons'], () => getActionButtons(), { ...(context && { - select: (data) => data.filter((button) => button.context === context), + select: (data) => + data.filter( + ( + button, + ): button is IUIActionButton & { + context: UIActionButtonContext extends infer X ? (X extends TContext ? X : never) : never; + } => button.context === context, + ), }), staleTime: Infinity, }); @@ -55,6 +64,8 @@ export const useAppActionButtons = (context?: `${UIActionButtonContext}`) => { export const useMessageboxAppsActionButtons = () => { const result = useAppActionButtons('messageBoxAction'); const actionManager = useUiKitActionManager(); + const dispatchToastMessage = useToastMessageDispatch(); + const { t } = useTranslation(); const applyButtonFilters = useApplyButtonFilters(); @@ -69,19 +80,31 @@ export const useMessageboxAppsActionButtons = () => { id: getIdForActionButton(action), label: Utilities.getI18nKeyForApp(action.labelI18n, action.appId), action: (params) => { - void actionManager.triggerActionButtonAction({ - rid: params.rid, - tmid: params.tmid, - actionId: action.actionId, - appId: action.appId, - payload: { context: action.context, message: params.chat.composer?.text }, - }); + void actionManager + .emitInteraction(action.appId, { + type: 'actionButton', + rid: params.rid, + tmid: params.tmid, + actionId: action.actionId, + payload: { context: action.context, message: params.chat.composer?.text ?? '' }, + }) + .catch(async (reason) => { + if (reason instanceof UiKitTriggerTimeoutError) { + dispatchToastMessage({ + type: 'error', + message: t('UIKit_Interaction_Timeout'), + }); + return; + } + + return reason; + }); }, }; return item; }), - [actionManager, applyButtonFilters, result.data], + [actionManager, applyButtonFilters, dispatchToastMessage, result.data, t], ); return { ...result, @@ -92,6 +115,8 @@ export const useMessageboxAppsActionButtons = () => { export const useUserDropdownAppsActionButtons = () => { const result = useAppActionButtons('userDropdownAction'); const actionManager = useUiKitActionManager(); + const dispatchToastMessage = useToastMessageDispatch(); + const { t } = useTranslation(); const applyButtonFilters = useApplyButtonAuthFilter(); @@ -107,15 +132,27 @@ export const useUserDropdownAppsActionButtons = () => { // icon: action.icon as GenericMenuItemProps['icon'], content: action.labelI18n, onClick: () => { - actionManager.triggerActionButtonAction({ - actionId: action.actionId, - appId: action.appId, - payload: { context: action.context }, - }); + void actionManager + .emitInteraction(action.appId, { + type: 'actionButton', + actionId: action.actionId, + payload: { context: action.context }, + }) + .catch(async (reason) => { + if (reason instanceof UiKitTriggerTimeoutError) { + dispatchToastMessage({ + type: 'error', + message: t('UIKit_Interaction_Timeout'), + }); + return; + } + + return reason; + }); }, }; }), - [actionManager, applyButtonFilters, result.data], + [actionManager, applyButtonFilters, dispatchToastMessage, result.data, t], ); return { ...result, @@ -127,6 +164,8 @@ export const useMessageActionAppsActionButtons = (context?: MessageActionContext const result = useAppActionButtons('messageAction'); const actionManager = useUiKitActionManager(); const applyButtonFilters = useApplyButtonFilters(); + const dispatchToastMessage = useToastMessageDispatch(); + const { t } = useTranslation(); const data = useMemo( () => result.data @@ -148,20 +187,32 @@ export const useMessageActionAppsActionButtons = (context?: MessageActionContext type: 'apps', variant: action.variant, action: (_, params) => { - void actionManager.triggerActionButtonAction({ - rid: params.message.rid, - tmid: params.message.tmid, - mid: params.message._id, - actionId: action.actionId, - appId: action.appId, - payload: { context: action.context }, - }); + void actionManager + .emitInteraction(action.appId, { + type: 'actionButton', + rid: params.message.rid, + tmid: params.message.tmid, + mid: params.message._id, + actionId: action.actionId, + payload: { context: action.context }, + }) + .catch(async (reason) => { + if (reason instanceof UiKitTriggerTimeoutError) { + dispatchToastMessage({ + type: 'error', + message: t('UIKit_Interaction_Timeout'), + }); + return; + } + + return reason; + }); }, }; return item; }), - [actionManager, applyButtonFilters, context, result.data], + [actionManager, applyButtonFilters, context, dispatchToastMessage, result.data, t], ); return { ...result, diff --git a/apps/meteor/client/hooks/useAppUiKitInteraction.ts b/apps/meteor/client/hooks/useAppUiKitInteraction.ts index 84849f592a48..e620d34a141d 100644 --- a/apps/meteor/client/hooks/useAppUiKitInteraction.ts +++ b/apps/meteor/client/hooks/useAppUiKitInteraction.ts @@ -1,16 +1,8 @@ -import type { UIKitInteractionType } from '@rocket.chat/apps-engine/definition/uikit'; +import type { UiKit } from '@rocket.chat/core-typings'; import { useStream, useUserId } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; -export const useAppUiKitInteraction = ( - handlePayloadUserInteraction: ( - type: UIKitInteractionType, - data: { - triggerId: string; - appId: string; - }, - ) => void, -) => { +export const useAppUiKitInteraction = (handleServerInteraction: (interaction: UiKit.ServerInteraction) => void) => { const notifyUser = useStream('notify-user'); const uid = useUserId(); @@ -19,8 +11,9 @@ export const useAppUiKitInteraction = ( return; } - return notifyUser(`${uid}/uiInteraction`, ({ type, ...data }) => { - handlePayloadUserInteraction(type, data); + return notifyUser(`${uid}/uiInteraction`, (interaction) => { + // @ts-ignore + handleServerInteraction(interaction); }); - }, [notifyUser, uid, handlePayloadUserInteraction]); + }, [notifyUser, uid, handleServerInteraction]); }; diff --git a/apps/meteor/client/lib/banners.ts b/apps/meteor/client/lib/banners.ts index 89310da2e3c7..91185450a21a 100644 --- a/apps/meteor/client/lib/banners.ts +++ b/apps/meteor/client/lib/banners.ts @@ -1,4 +1,4 @@ -import type { UiKitBannerPayload } from '@rocket.chat/core-typings'; +import type { UiKit } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import type { Keys as IconName } from '@rocket.chat/icons'; @@ -15,7 +15,7 @@ export type LegacyBannerPayload = { onClose?: () => Promise | void; }; -type BannerPayload = LegacyBannerPayload | UiKitBannerPayload; +type BannerPayload = LegacyBannerPayload | UiKit.BannerView; export const isLegacyPayload = (payload: BannerPayload): payload is LegacyBannerPayload => !('blocks' in payload); @@ -35,7 +35,7 @@ export const open = (payload: BannerPayload): void => { if (isLegacyPayload(_payload)) { return _payload.id === (payload as LegacyBannerPayload).id; } - return (_payload as UiKitBannerPayload).viewId === (payload as UiKitBannerPayload).viewId; + return _payload.viewId === (payload as UiKit.BannerView).viewId; }); if (index === -1) { diff --git a/apps/meteor/client/lib/chats/flows/processSlashCommand.ts b/apps/meteor/client/lib/chats/flows/processSlashCommand.ts index 1551f8eb1f57..c9922162a67c 100644 --- a/apps/meteor/client/lib/chats/flows/processSlashCommand.ts +++ b/apps/meteor/client/lib/chats/flows/processSlashCommand.ts @@ -4,7 +4,7 @@ import { escapeHTML } from '@rocket.chat/string-helpers'; import { hasAtLeastOnePermission } from '../../../../app/authorization/client'; import { settings } from '../../../../app/settings/client'; -import { generateTriggerId } from '../../../../app/ui-message/client/ActionManager'; +import { actionManager } from '../../../../app/ui-message/client/ActionManager'; import { slashCommands } from '../../../../app/utils/client'; import { sdk } from '../../../../app/utils/client/lib/SDKClient'; import { t } from '../../../../app/utils/lib/i18n'; @@ -78,7 +78,7 @@ export const processSlashCommand = async (chat: ChatAPI, message: IMessage): Pro params: [{ eventName: 'slashCommandsStats', timestamp: Date.now(), command: commandName }], }); - const triggerId = generateTriggerId(appId); + const triggerId = actionManager.generateTriggerId(appId); const data = { cmd: commandName, diff --git a/apps/meteor/client/lib/utils/preventSyntheticEvent.ts b/apps/meteor/client/lib/utils/preventSyntheticEvent.ts new file mode 100644 index 000000000000..773b53a1a88c --- /dev/null +++ b/apps/meteor/client/lib/utils/preventSyntheticEvent.ts @@ -0,0 +1,9 @@ +import type { SyntheticEvent } from 'react'; + +export const preventSyntheticEvent = (e: SyntheticEvent): void => { + if (e) { + (e.nativeEvent || e).stopImmediatePropagation(); + e.stopPropagation(); + e.preventDefault(); + } +}; diff --git a/apps/meteor/client/polyfills/index.ts b/apps/meteor/client/polyfills/index.ts index f07d828a4602..bc91265b04ba 100644 --- a/apps/meteor/client/polyfills/index.ts +++ b/apps/meteor/client/polyfills/index.ts @@ -4,3 +4,4 @@ import './childNodeRemove'; import './cssVars'; import './customEventPolyfill'; import './hoverTouchClick'; +import './promiseFinally'; diff --git a/apps/meteor/client/polyfills/promiseFinally.ts b/apps/meteor/client/polyfills/promiseFinally.ts new file mode 100644 index 000000000000..ab826c2bd0ba --- /dev/null +++ b/apps/meteor/client/polyfills/promiseFinally.ts @@ -0,0 +1,16 @@ +if (!Promise.prototype.finally) { + // eslint-disable-next-line no-extend-native + Promise.prototype.finally = function (callback) { + if (typeof callback !== 'function') { + return this.then(callback, callback); + } + const P = (this.constructor as PromiseConstructor) || Promise; + return this.then( + (value) => P.resolve(callback()).then(() => value), + (err) => + P.resolve(callback()).then(() => { + throw err; + }), + ); + }; +} diff --git a/apps/meteor/client/providers/ActionManagerProvider.tsx b/apps/meteor/client/providers/ActionManagerProvider.tsx index 8faa55260f13..e8961ec357e9 100644 --- a/apps/meteor/client/providers/ActionManagerProvider.tsx +++ b/apps/meteor/client/providers/ActionManagerProvider.tsx @@ -2,7 +2,7 @@ import { ActionManagerContext } from '@rocket.chat/ui-contexts'; import type { ReactNode, ReactElement } from 'react'; import React from 'react'; -import * as ActionManager from '../../app/ui-message/client/ActionManager'; +import { actionManager } from '../../app/ui-message/client/ActionManager'; import { useAppActionButtons } from '../hooks/useAppActionButtons'; import { useAppSlashCommands } from '../hooks/useAppSlashCommands'; import { useAppTranslations } from '../hooks/useAppTranslations'; @@ -16,9 +16,9 @@ const ActionManagerProvider = ({ children }: ActionManagerProviderProps): ReactE useAppTranslations(); useAppActionButtons(); useAppSlashCommands(); - useAppUiKitInteraction(ActionManager.handlePayloadUserInteraction); + useAppUiKitInteraction(actionManager.handleServerInteraction.bind(actionManager)); - return {children}; + return {children}; }; export default ActionManagerProvider; diff --git a/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx b/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx index 114cde52c1d2..500ae78a6d26 100644 --- a/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx +++ b/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx @@ -75,7 +75,7 @@ const ContextMessage = ({ ) : ( message.msg )} - {message.blocks && } + {message.blocks && } {message.attachments && } diff --git a/apps/meteor/client/views/banners/BannerRegion.tsx b/apps/meteor/client/views/banners/BannerRegion.tsx index c5394f787229..b79c156db842 100644 --- a/apps/meteor/client/views/banners/BannerRegion.tsx +++ b/apps/meteor/client/views/banners/BannerRegion.tsx @@ -22,7 +22,7 @@ const BannerRegion = (): ReactElement | null => { return ; } - return ; + return ; }; export default BannerRegion; diff --git a/apps/meteor/client/views/banners/UiKitBanner.tsx b/apps/meteor/client/views/banners/UiKitBanner.tsx index 7cb52dd8d3c9..64a602d548dc 100644 --- a/apps/meteor/client/views/banners/UiKitBanner.tsx +++ b/apps/meteor/client/views/banners/UiKitBanner.tsx @@ -1,55 +1,93 @@ -import type { UIKitActionEvent, UiKitBannerProps } from '@rocket.chat/core-typings'; +import type { UiKit } from '@rocket.chat/core-typings'; import { Banner, Icon } from '@rocket.chat/fuselage'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { UiKitContext, bannerParser, UiKitBanner as UiKitBannerSurfaceRender, UiKitComponent } from '@rocket.chat/fuselage-ui-kit'; -import type { Keys as IconName } from '@rocket.chat/icons'; -import type { LayoutBlock } from '@rocket.chat/ui-kit'; -import type { FC, ReactElement, ContextType } from 'react'; +import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import type { ReactElement, ContextType } from 'react'; import React, { useMemo } from 'react'; -import { useUIKitHandleAction } from '../../UIKit/hooks/useUIKitHandleAction'; -import { useUIKitHandleClose } from '../../UIKit/hooks/useUIKitHandleClose'; -import { useUIKitStateManager } from '../../UIKit/hooks/useUIKitStateManager'; +import { useUiKitActionManager } from '../../UIKit/hooks/useUiKitActionManager'; +import { useUiKitView } from '../../UIKit/hooks/useUiKitView'; import MarkdownText from '../../components/MarkdownText'; -import * as banners from '../../lib/banners'; // TODO: move this to fuselage-ui-kit itself bannerParser.mrkdwn = ({ text }): ReactElement => ; -const UiKitBanner: FC = ({ payload }) => { - const state = useUIKitStateManager(payload); +type UiKitBannerProps = { + key: UiKit.BannerView['viewId']; // force re-mount when viewId changes + initialView: UiKit.BannerView; +}; + +const UiKitBanner = ({ initialView }: UiKitBannerProps) => { + const { view, values, state } = useUiKitView(initialView); const icon = useMemo(() => { - if (state.icon) { - return ; + if (view.icon) { + return ; } return null; - }, [state.icon]); + }, [view.icon]); - const handleClose = useUIKitHandleClose(state, () => banners.close()); + const dispatchToastMessage = useToastMessageDispatch(); + const handleClose = useMutableCallback(() => { + void actionManager + .emitInteraction(view.appId, { + type: 'viewClosed', + payload: { + viewId: view.viewId, + view: { + ...view, + id: view.viewId, + state, + }, + isCleared: true, + }, + }) + .catch((error) => { + dispatchToastMessage({ type: 'error', message: error }); + return Promise.reject(error); + }) + .finally(() => { + actionManager.disposeView(view.viewId); + }); + }); - const action = useUIKitHandleAction(state); + const actionManager = useUiKitActionManager(); - const contextValue = useMemo>( - () => ({ - action: async (event): Promise => { - if (!event.viewId) { + const contextValue = useMemo( + (): ContextType => ({ + action: async ({ appId, viewId, actionId, blockId, value }) => { + if (!appId || !viewId) { return; } - await action(event as UIKitActionEvent); - banners.closeById(state.viewId); + + await actionManager.emitInteraction(appId, { + type: 'blockAction', + actionId, + container: { + type: 'view', + id: viewId, + }, + payload: { + blockId, + value, + }, + }); + + actionManager.disposeView(view.viewId); }, state: (): void => undefined, - appId: state.appId, - values: {}, + appId: view.appId, + values: values as any, }), - [action, state.appId, state.viewId], + [view, values, actionManager], ); return ( - + - + ); diff --git a/apps/meteor/client/views/banners/hooks/useRemoteBanners.ts b/apps/meteor/client/views/banners/hooks/useRemoteBanners.ts index ff42d4ae9ace..ebed89e06037 100644 --- a/apps/meteor/client/views/banners/hooks/useRemoteBanners.ts +++ b/apps/meteor/client/views/banners/hooks/useRemoteBanners.ts @@ -1,5 +1,5 @@ import { BannerPlatform } from '@rocket.chat/core-typings'; -import type { IBanner, Serialized, UiKitBannerPayload } from '@rocket.chat/core-typings'; +import type { IBanner, Serialized, UiKit } from '@rocket.chat/core-typings'; import { useEndpoint, useStream, useUserId, ServerContext } from '@rocket.chat/ui-contexts'; import { useContext, useEffect } from 'react'; @@ -22,7 +22,7 @@ export const useRemoteBanners = () => { const { signal } = controller; - const mapBanner = (banner: Serialized): UiKitBannerPayload => ({ + const mapBanner = (banner: Serialized): UiKit.BannerView => ({ ...banner.view, viewId: banner.view.viewId || banner._id, }); diff --git a/apps/meteor/client/views/modal/uikit/ModalBlock.tsx b/apps/meteor/client/views/modal/uikit/ModalBlock.tsx index 7993355206e7..bd0876fc49ad 100644 --- a/apps/meteor/client/views/modal/uikit/ModalBlock.tsx +++ b/apps/meteor/client/views/modal/uikit/ModalBlock.tsx @@ -1,4 +1,4 @@ -import type { IUIKitSurface } from '@rocket.chat/apps-engine/definition/uikit'; +import type { UiKit } from '@rocket.chat/core-typings'; import { Modal, AnimatedVisibility, Button, Box } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { UiKitComponent, UiKitModal, modalParser } from '@rocket.chat/fuselage-ui-kit'; @@ -38,7 +38,7 @@ const focusableElementsStringInvalid = ` [contenteditable]:invalid`; type ModalBlockParams = { - view: IUIKitSurface & { showIcon?: boolean }; + view: UiKit.ModalView; errors: any; appId: string; onSubmit: FormEventHandler; @@ -55,7 +55,7 @@ const KeyboardCode = new Map([ ['TAB', 9], ]); -const ModalBlock = ({ view, errors, appId, onSubmit, onClose, onCancel }: ModalBlockParams): ReactElement => { +const ModalBlock = ({ view, errors, onSubmit, onClose, onCancel }: ModalBlockParams): ReactElement => { const id = `modal_id_${useUniqueId()}`; const ref = useRef(null); @@ -165,7 +165,7 @@ const ModalBlock = ({ view, errors, appId, onSubmit, onClose, onCancel }: ModalB - {view.showIcon ? : null} + {view.showIcon ? : null} {modalParser.text(view.title, BlockContext.NONE, 0)} @@ -182,7 +182,7 @@ const ModalBlock = ({ view, errors, appId, onSubmit, onClose, onCancel }: ModalB )} {view.submit && ( - )} diff --git a/apps/meteor/client/views/modal/uikit/UiKitModal.tsx b/apps/meteor/client/views/modal/uikit/UiKitModal.tsx index b985f94b09b9..52aaa49ed009 100644 --- a/apps/meteor/client/views/modal/uikit/UiKitModal.tsx +++ b/apps/meteor/client/views/modal/uikit/UiKitModal.tsx @@ -1,139 +1,130 @@ -import { UIKitIncomingInteractionContainerType } from '@rocket.chat/apps-engine/definition/uikit/UIKitIncomingInteractionContainer'; +import type { UiKit } from '@rocket.chat/core-typings'; import { useDebouncedCallback, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { UiKitContext } from '@rocket.chat/fuselage-ui-kit'; import { MarkupInteractionContext } from '@rocket.chat/gazzodown'; -import type { LayoutBlock } from '@rocket.chat/ui-kit'; -import type { ContextType, ReactElement, ReactEventHandler } from 'react'; -import React from 'react'; +import type { ContextType, FormEvent } from 'react'; +import React, { useMemo } from 'react'; -import { useUiKitActionManager } from '../../../hooks/useUiKitActionManager'; +import { useUiKitActionManager } from '../../../UIKit/hooks/useUiKitActionManager'; +import { useUiKitView } from '../../../UIKit/hooks/useUiKitView'; import { detectEmoji } from '../../../lib/utils/detectEmoji'; +import { preventSyntheticEvent } from '../../../lib/utils/preventSyntheticEvent'; import ModalBlock from './ModalBlock'; -import type { ActionManagerState } from './hooks/useActionManagerState'; -import { useActionManagerState } from './hooks/useActionManagerState'; -import { useValues } from './hooks/useValues'; -const UiKitModal = (props: ActionManagerState): ReactElement => { - const actionManager = useUiKitActionManager(); - const state = useActionManagerState(props); - - const { appId, viewId, mid: _mid, errors, view } = state; - - const [values, updateValues] = useValues(view.blocks as LayoutBlock[]); +type UiKitModalProps = { + key: UiKit.ModalView['id']; // force re-mount when viewId changes + initialView: UiKit.ModalView; +}; - const groupStateByBlockId = (values: { value: unknown; blockId: string }[]) => - Object.entries(values).reduce((obj, [key, { blockId, value }]) => { - obj[blockId] = obj[blockId] || {}; - obj[blockId][key] = value; +const UiKitModal = ({ initialView }: UiKitModalProps) => { + const actionManager = useUiKitActionManager(); + const { view, errors, values, updateValues, state } = useUiKitView(initialView); - return obj; - }, {}); + const emitInteraction = useMemo(() => actionManager.emitInteraction.bind(actionManager), [actionManager]); + const debouncedEmitInteraction = useDebouncedCallback(emitInteraction, 700); - const prevent: ReactEventHandler = (e) => { - if (e) { - (e.nativeEvent || e).stopImmediatePropagation(); - e.stopPropagation(); - e.preventDefault(); - } - }; + // TODO: this structure is atrociously wrong; we should revisit this + const contextValue = useMemo( + (): ContextType => ({ + action: async ({ actionId, viewId, appId, dispatchActionConfig, blockId, value }) => { + if (!appId || !viewId) { + return; + } - const debouncedBlockAction = useDebouncedCallback((actionId, appId, value, blockId, mid) => { - actionManager.triggerBlockAction({ - container: { - type: UIKitIncomingInteractionContainerType.VIEW, - id: viewId, - }, - actionId, - appId, - value, - blockId, - mid, - }); - }, 700); + const emit = dispatchActionConfig?.includes('on_character_entered') ? debouncedEmitInteraction : emitInteraction; - // TODO: this structure is atrociously wrong; we should revisit this - const context: ContextType = { - // @ts-expect-error Property 'mid' does not exist on type 'ActionParams'. - action: ({ actionId, appId, value, blockId, mid = _mid, dispatchActionConfig }) => { - if (Array.isArray(dispatchActionConfig) && dispatchActionConfig.includes('on_character_entered')) { - debouncedBlockAction(actionId, appId, value, blockId, mid); - } else { - actionManager.triggerBlockAction({ + await emit(appId, { + type: 'blockAction', + actionId, container: { - type: UIKitIncomingInteractionContainerType.VIEW, + type: 'view', id: viewId, }, + payload: { + blockId, + value, + }, + }); + }, + state: ({ actionId, value, /* ,appId, */ blockId = 'default' }) => { + updateValues({ actionId, - appId, - value, - blockId, - mid, + payload: { + blockId, + value, + }, }); - } - }, + }, + ...view, + values, + viewId: view.id, + }), + [debouncedEmitInteraction, emitInteraction, updateValues, values, view], + ); - state: ({ actionId, value, /* ,appId, */ blockId = 'default' }) => { - updateValues({ - actionId, + const handleSubmit = useMutableCallback((e: FormEvent) => { + preventSyntheticEvent(e); + void actionManager + .emitInteraction(view.appId, { + type: 'viewSubmit', payload: { - blockId, - value, + view: { + ...view, + state, + }, }, + viewId: view.id, + }) + .finally(() => { + actionManager.disposeView(view.id); }); - }, - ...state, - values, - }; - - const handleSubmit = useMutableCallback((e) => { - prevent(e); - actionManager.triggerSubmitView({ - viewId, - appId, - payload: { - view: { - ...view, - id: viewId, - state: groupStateByBlockId(values), - }, - }, - }); }); - const handleCancel = useMutableCallback((e) => { - prevent(e); - actionManager.triggerCancel({ - viewId, - appId, - view: { - ...view, - id: viewId, - state: groupStateByBlockId(values), - }, - }); + const handleCancel = useMutableCallback((e: FormEvent) => { + preventSyntheticEvent(e); + void actionManager + .emitInteraction(view.appId, { + type: 'viewClosed', + payload: { + viewId: view.id, + view: { + ...view, + state, + }, + isCleared: false, + }, + }) + .finally(() => { + actionManager.disposeView(view.id); + }); }); const handleClose = useMutableCallback(() => { - actionManager.triggerCancel({ - viewId, - appId, - view: { - ...view, - id: viewId, - state: groupStateByBlockId(values), - }, - isCleared: true, - }); + void actionManager + .emitInteraction(view.appId, { + type: 'viewClosed', + payload: { + viewId: view.id, + view: { + ...view, + state, + }, + isCleared: true, + }, + }) + .finally(() => { + actionManager.disposeView(view.id); + }); }); return ( - + - + ); diff --git a/apps/meteor/client/views/modal/uikit/getButtonStyle.ts b/apps/meteor/client/views/modal/uikit/getButtonStyle.ts index 4a78cb5e250a..89b489fd66fc 100644 --- a/apps/meteor/client/views/modal/uikit/getButtonStyle.ts +++ b/apps/meteor/client/views/modal/uikit/getButtonStyle.ts @@ -1,6 +1,6 @@ -import type { IUIKitSurface } from '@rocket.chat/apps-engine/definition/uikit'; +import type { ButtonElement } from '@rocket.chat/ui-kit'; // TODO: Move to fuselage-ui-kit -export const getButtonStyle = (view: IUIKitSurface): { danger: boolean } | { primary: boolean } => { - return view.submit?.style === 'danger' ? { danger: true } : { primary: true }; +export const getButtonStyle = (buttonElement: ButtonElement): { danger: boolean } | { primary: boolean } => { + return buttonElement?.style === 'danger' ? { danger: true } : { primary: true }; }; diff --git a/apps/meteor/client/views/modal/uikit/hooks/useActionManagerState.ts b/apps/meteor/client/views/modal/uikit/hooks/useActionManagerState.ts deleted file mode 100644 index fb1da19010e3..000000000000 --- a/apps/meteor/client/views/modal/uikit/hooks/useActionManagerState.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { IUIKitSurface } from '@rocket.chat/apps-engine/definition/uikit'; -import { useEffect, useState } from 'react'; - -import { useUiKitActionManager } from '../../../../hooks/useUiKitActionManager'; - -export type ActionManagerState = { - viewId: string; - type: 'errors' | string; - appId: string; - mid: string; - errors: Record; - view: IUIKitSurface; -}; - -export const useActionManagerState = (initialState: ActionManagerState) => { - const actionManager = useUiKitActionManager(); - const [state, setState] = useState(initialState); - - const { viewId } = state; - - useEffect(() => { - const handleUpdate = ({ type, errors, ...data }: ActionManagerState) => { - if (type === 'errors') { - setState((state) => ({ ...state, errors, type })); - return; - } - - setState({ ...data, type, errors }); - }; - - actionManager.on(viewId, handleUpdate); - - return () => { - actionManager.off(viewId, handleUpdate); - }; - }, [actionManager, viewId]); - - return state; -}; diff --git a/apps/meteor/client/views/modal/uikit/hooks/useValues.ts b/apps/meteor/client/views/modal/uikit/hooks/useValues.ts deleted file mode 100644 index 34a8eb0c5ae2..000000000000 --- a/apps/meteor/client/views/modal/uikit/hooks/useValues.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import type { LayoutBlock } from '@rocket.chat/ui-kit'; -import { useReducer } from 'react'; - -type LayoutBlockWithElement = Extract; -type LayoutBlockWithElements = Extract; -type ElementFromLayoutBlock = LayoutBlockWithElement['element'] | LayoutBlockWithElements['elements'][number]; - -const hasElementInBlock = (block: LayoutBlock): block is LayoutBlockWithElement => 'element' in block; -const hasElementsInBlock = (block: LayoutBlock): block is LayoutBlockWithElements => 'elements' in block; -const hasInitialValueAndActionId = ( - element: ElementFromLayoutBlock, -): element is Extract & { initialValue: unknown } => - 'initialValue' in element && 'actionId' in element && typeof element.actionId === 'string' && !!element?.initialValue; - -const extractValue = (element: ElementFromLayoutBlock, obj: Record, blockId?: string) => { - if (hasInitialValueAndActionId(element)) { - obj[element.actionId] = { value: element.initialValue, blockId }; - } -}; - -const reduceBlocks = (obj: Record, block: LayoutBlock) => { - if (hasElementInBlock(block)) { - extractValue(block.element, obj, block.blockId); - } - if (hasElementsInBlock(block)) { - for (const element of block.elements) { - extractValue(element, obj, block.blockId); - } - } - - return obj; -}; - -export const useValues = (blocks: LayoutBlock[]) => { - const reducer = useMutableCallback((values, { actionId, payload }) => ({ - ...values, - [actionId]: payload, - })); - - const initializer = useMutableCallback((blocks: LayoutBlock[]) => { - const obj: Record = {}; - - return blocks.reduce(reduceBlocks, obj); - }); - - return useReducer(reducer, blocks, initializer); -}; diff --git a/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx b/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx index 12342a6258a3..dab42e58e300 100644 --- a/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx +++ b/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx @@ -105,7 +105,7 @@ const ContactHistoryMessage: FC<{ )} - {message.blocks && } + {message.blocks && } {message.attachments && } diff --git a/apps/meteor/client/views/room/Room.tsx b/apps/meteor/client/views/room/Room.tsx index d53254647483..d8cf86dbbb48 100644 --- a/apps/meteor/client/views/room/Room.tsx +++ b/apps/meteor/client/views/room/Room.tsx @@ -23,7 +23,7 @@ const Room = (): ReactElement => { const toolbox = useRoomToolbox(); - const appsContextualBarContext = useAppsContextualBar(); + const contextualBarView = useAppsContextualBar(); return ( @@ -41,16 +41,11 @@ const Room = (): ReactElement => { )) || - (appsContextualBarContext && ( + (contextualBarView && ( }> - + diff --git a/apps/meteor/client/views/room/contextualBar/uikit/UiKitContextualBar.tsx b/apps/meteor/client/views/room/contextualBar/uikit/UiKitContextualBar.tsx index 6f3b803ebf84..543a36443185 100644 --- a/apps/meteor/client/views/room/contextualBar/uikit/UiKitContextualBar.tsx +++ b/apps/meteor/client/views/room/contextualBar/uikit/UiKitContextualBar.tsx @@ -1,15 +1,4 @@ -import type { - IUIKitContextualBarInteraction, - IUIKitErrorInteraction, - IUIKitSurface, - IInputElement, - IInputBlock, - IBlock, - IBlockElement, - IActionsBlock, -} from '@rocket.chat/apps-engine/definition/uikit'; -import { InputElementDispatchAction } from '@rocket.chat/apps-engine/definition/uikit'; -import { UIKitIncomingInteractionContainerType } from '@rocket.chat/apps-engine/definition/uikit/UIKitIncomingInteractionContainer'; +import type { UiKit } from '@rocket.chat/core-typings'; import { Avatar, Box, Button, ButtonGroup, ContextualbarFooter, ContextualbarHeader, ContextualbarTitle } from '@rocket.chat/fuselage'; import { useDebouncedCallback, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { @@ -18,237 +7,139 @@ import { contextualBarParser, UiKitContext, } from '@rocket.chat/fuselage-ui-kit'; -import type { LayoutBlock } from '@rocket.chat/ui-kit'; -import { BlockContext, type Block } from '@rocket.chat/ui-kit'; -import type { Dispatch, SyntheticEvent, ContextType } from 'react'; -import React, { memo, useState, useEffect, useReducer } from 'react'; +import { BlockContext } from '@rocket.chat/ui-kit'; +import type { ContextType, FormEvent, UIEvent } from 'react'; +import React, { memo, useMemo } from 'react'; import { getURL } from '../../../../../app/utils/client'; +import { useUiKitActionManager } from '../../../../UIKit/hooks/useUiKitActionManager'; +import { useUiKitView } from '../../../../UIKit/hooks/useUiKitView'; import { ContextualbarClose, ContextualbarScrollableContent } from '../../../../components/Contextualbar'; -import { useUiKitActionManager } from '../../../../hooks/useUiKitActionManager'; +import { preventSyntheticEvent } from '../../../../lib/utils/preventSyntheticEvent'; import { getButtonStyle } from '../../../modal/uikit/getButtonStyle'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; -type FieldStateValue = string | Array | undefined; -type FieldState = { value: FieldStateValue; blockId: string }; -type InputFieldStateTuple = [string, FieldState]; -type InputFieldStateObject = { [key: string]: FieldState }; -type InputFieldStateByBlockId = { [blockId: string]: { [actionId: string]: FieldStateValue } }; -type ActionParams = { - blockId: string; - appId: string; - actionId: string; - value: unknown; - viewId?: string; - dispatchActionConfig?: InputElementDispatchAction[]; +type UiKitContextualBarProps = { + key: UiKit.ContextualBarView['id']; // force re-mount when viewId changes + initialView: UiKit.ContextualBarView; }; -type ViewState = IUIKitContextualBarInteraction & { - errors?: { [field: string]: string }; -}; - -const isInputBlock = (block: any): block is IInputBlock => block?.element?.initialValue; - -const useValues = (view: IUIKitSurface): [any, Dispatch] => { - const reducer = useMutableCallback((values, { actionId, payload }) => ({ - ...values, - [actionId]: payload, - })); - - const initializer = useMutableCallback(() => { - const filterInputFields = (block: IBlock | Block): boolean => { - if (isInputBlock(block)) { - return true; - } - - if ( - ((block as IActionsBlock).elements as IInputElement[])?.filter((element) => filterInputFields({ element } as IInputBlock)).length - ) { - return true; - } - - return false; - }; - - const mapElementToState = (block: IBlock | Block): InputFieldStateTuple | InputFieldStateTuple[] => { - if (isInputBlock(block)) { - const { element, blockId } = block; - return [element.actionId, { value: element.initialValue, blockId } as FieldState]; - } - - const { elements, blockId }: { elements: IBlockElement[]; blockId?: string } = block as IActionsBlock; - - return elements - .filter((element) => filterInputFields({ element } as IInputBlock)) - .map((element) => mapElementToState({ element, blockId } as IInputBlock)) as InputFieldStateTuple[]; - }; - - return view.blocks - .filter(filterInputFields) - .map(mapElementToState) - .reduce((obj: InputFieldStateObject, el: InputFieldStateTuple | InputFieldStateTuple[]) => { - if (Array.isArray(el[0])) { - return { ...obj, ...Object.fromEntries(el as InputFieldStateTuple[]) }; - } - - const [key, value] = el as InputFieldStateTuple; - return { ...obj, [key]: value }; - }, {} as InputFieldStateObject); - }); - - return useReducer(reducer, null, initializer); -}; - -const UiKitContextualBar = ({ - viewId, - roomId, - payload, - appId, -}: { - viewId: string; - roomId: string; - payload: IUIKitContextualBarInteraction; - appId: string; -}): JSX.Element => { - const actionManager = useUiKitActionManager(); +const UiKitContextualBar = ({ initialView }: UiKitContextualBarProps): JSX.Element => { const { closeTab } = useRoomToolbox(); + const actionManager = useUiKitActionManager(); - const [state, setState] = useState(payload); - const { view } = state; - const [values, updateValues] = useValues(view); - - useEffect(() => { - const handleUpdate = ({ type, ...data }: IUIKitContextualBarInteraction | IUIKitErrorInteraction): void => { - if (type === 'errors') { - const { errors } = data as Omit; - setState((state: ViewState) => ({ ...state, errors })); - return; - } - - setState(data as IUIKitContextualBarInteraction); - }; - - actionManager.on(viewId, handleUpdate); - - return (): void => { - actionManager.off(viewId, handleUpdate); - }; - }, [actionManager, state, viewId]); + const { view, values, updateValues, state } = useUiKitView(initialView); - const groupStateByBlockId = (obj: InputFieldStateObject): InputFieldStateByBlockId => - Object.entries(obj).reduce((obj: InputFieldStateByBlockId, [key, { blockId, value }]: InputFieldStateTuple) => { - obj[blockId] = obj[blockId] || {}; - obj[blockId][key] = value; - return obj; - }, {} as InputFieldStateByBlockId); + const emitInteraction = useMemo(() => actionManager.emitInteraction.bind(actionManager), [actionManager]); + const debouncedEmitInteraction = useDebouncedCallback(emitInteraction, 700); - const prevent = (e: SyntheticEvent): void => { - if (e) { - (e.nativeEvent || e).stopImmediatePropagation(); - e.stopPropagation(); - e.preventDefault(); - } - }; + const contextValue = useMemo( + (): ContextType => ({ + action: async ({ appId, viewId, actionId, dispatchActionConfig, blockId, value }): Promise => { + if (!appId || !viewId) { + return; + } - const debouncedBlockAction = useDebouncedCallback(({ actionId, appId, value, blockId }: ActionParams) => { - actionManager.triggerBlockAction({ - container: { - type: UIKitIncomingInteractionContainerType.VIEW, - id: viewId, - }, - actionId, - appId, - value, - blockId, - }); - }, 700); + const emit = dispatchActionConfig?.includes('on_character_entered') ? debouncedEmitInteraction : emitInteraction; - const context: ContextType = { - action: async ({ actionId, appId, value, blockId, dispatchActionConfig }: ActionParams): Promise => { - if (Array.isArray(dispatchActionConfig) && dispatchActionConfig.includes(InputElementDispatchAction.ON_CHARACTER_ENTERED)) { - await debouncedBlockAction({ actionId, appId, value, blockId }); - } else { - await actionManager.triggerBlockAction({ + await emit(appId, { + type: 'blockAction', + actionId, container: { - type: UIKitIncomingInteractionContainerType.VIEW, + type: 'view', id: viewId, }, + payload: { + blockId, + value, + }, + }); + }, + state: ({ actionId, value, blockId = 'default' }) => { + updateValues({ actionId, - appId, - rid: roomId, - value, - blockId, + payload: { + blockId, + value, + }, }); - } - }, - state: ({ actionId, value, blockId = 'default' }: ActionParams): void => { - updateValues({ - actionId, - payload: { - blockId, - value, - }, - }); - }, - ...state, - values, - } as ContextType; + }, + ...view, + values, + viewId: view.id, + }), + [debouncedEmitInteraction, emitInteraction, updateValues, values, view], + ); - const handleSubmit = useMutableCallback((e) => { - prevent(e); + const handleSubmit = useMutableCallback((e: FormEvent) => { + preventSyntheticEvent(e); closeTab(); - actionManager.triggerSubmitView({ - viewId, - appId, - payload: { - view: { - ...view, - id: viewId, - state: groupStateByBlockId(values), + void actionManager + .emitInteraction(view.appId, { + type: 'viewSubmit', + payload: { + view: { + ...view, + state, + }, }, - }, - }); + viewId: view.id, + }) + .finally(() => { + actionManager.disposeView(view.id); + }); }); - const handleCancel = useMutableCallback((e) => { - prevent(e); + const handleCancel = useMutableCallback((e: UIEvent) => { + preventSyntheticEvent(e); closeTab(); - return actionManager.triggerCancel({ - appId, - viewId, - view: { - ...view, - id: viewId, - state: groupStateByBlockId(values), - }, - }); + void actionManager + .emitInteraction(view.appId, { + type: 'viewClosed', + payload: { + viewId: view.id, + view: { + ...view, + state, + }, + isCleared: false, + }, + }) + .finally(() => { + actionManager.disposeView(view.id); + }); }); - const handleClose = useMutableCallback((e) => { - prevent(e); + const handleClose = useMutableCallback((e: UIEvent) => { + preventSyntheticEvent(e); closeTab(); - return actionManager.triggerCancel({ - appId, - viewId, - view: { - ...view, - id: viewId, - state: groupStateByBlockId(values), - }, - isCleared: true, - }); + void actionManager + .emitInteraction(view.appId, { + type: 'viewClosed', + payload: { + viewId: view.id, + view: { + ...view, + state, + }, + isCleared: true, + }, + }) + .finally(() => { + actionManager.disposeView(view.id); + }); }); return ( - + - + {contextualBarParser.text(view.title, BlockContext.NONE, 0)} {handleClose && } - + @@ -258,8 +149,9 @@ const UiKitContextualBar = ({ {contextualBarParser.text(view.close.text, BlockContext.NONE, 0)} )} + {view.submit && ( - )} diff --git a/apps/meteor/client/views/room/hooks/useAppsContextualBar.ts b/apps/meteor/client/views/room/hooks/useAppsContextualBar.ts index 6afa6c3a6f84..c039c434a48f 100644 --- a/apps/meteor/client/views/room/hooks/useAppsContextualBar.ts +++ b/apps/meteor/client/views/room/hooks/useAppsContextualBar.ts @@ -1,49 +1,35 @@ -import type { IUIKitContextualBarInteraction } from '@rocket.chat/apps-engine/definition/uikit'; import { useRouteParameter } from '@rocket.chat/ui-contexts'; -import { useEffect, useState } from 'react'; +import { useCallback } from 'react'; +import { useSyncExternalStore } from 'use-sync-external-store/shim'; -import { useUiKitActionManager } from '../../../hooks/useUiKitActionManager'; -import { useRoom } from '../contexts/RoomContext'; +import { useUiKitActionManager } from '../../../UIKit/hooks/useUiKitActionManager'; -type AppsContextualBarData = { - viewId: string; - roomId: string; - payload: IUIKitContextualBarInteraction; - appId: string; -}; - -export const useAppsContextualBar = (): AppsContextualBarData | undefined => { - const [payload, setPayload] = useState(); +export const useAppsContextualBar = () => { + const viewId = useRouteParameter('context'); const actionManager = useUiKitActionManager(); - const [appId, setAppId] = useState(); - const { _id: roomId } = useRoom(); + const getSnapshot = useCallback(() => { + if (!viewId) { + return undefined; + } - const viewId = useRouteParameter('context'); + return actionManager.getInteractionPayloadByViewId(viewId)?.view; + }, [actionManager, viewId]); - useEffect(() => { - if (viewId) { - setPayload(actionManager.getUserInteractionPayloadByViewId(viewId) as IUIKitContextualBarInteraction); - } + const subscribe = useCallback( + (handler: () => void) => { + if (!viewId) { + return () => undefined; + } - if (payload?.appId) { - setAppId(payload.appId); - } + actionManager.on(viewId, handler); + + return () => actionManager.off(viewId, handler); + }, + [actionManager, viewId], + ); + + const view = useSyncExternalStore(subscribe, getSnapshot); - return (): void => { - setPayload(undefined); - setAppId(undefined); - }; - }, [viewId, payload?.appId, actionManager]); - - if (viewId && payload && appId) { - return { - viewId, - roomId, - payload, - appId, - }; - } - - return undefined; + return view; }; diff --git a/apps/meteor/client/views/room/providers/hooks/useAppsRoomActions.ts b/apps/meteor/client/views/room/providers/hooks/useAppsRoomActions.ts index 935da2a23c46..f402304a936b 100644 --- a/apps/meteor/client/views/room/providers/hooks/useAppsRoomActions.ts +++ b/apps/meteor/client/views/room/providers/hooks/useAppsRoomActions.ts @@ -1,9 +1,12 @@ +import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { UiKitTriggerTimeoutError } from '../../../../../app/ui-message/client/UiKitTriggerTimeoutError'; import { Utilities } from '../../../../../ee/lib/misc/Utilities'; +import { useUiKitActionManager } from '../../../../UIKit/hooks/useUiKitActionManager'; import { useAppActionButtons } from '../../../../hooks/useAppActionButtons'; import { useApplyButtonFilters } from '../../../../hooks/useApplyButtonFilters'; -import { useUiKitActionManager } from '../../../../hooks/useUiKitActionManager'; import { useRoom } from '../../contexts/RoomContext'; import type { RoomToolboxActionConfig } from '../../contexts/RoomToolboxContext'; @@ -12,6 +15,8 @@ export const useAppsRoomActions = () => { const actionManager = useUiKitActionManager(); const applyButtonFilters = useApplyButtonFilters(); const room = useRoom(); + const { t } = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); return useMemo( () => @@ -25,16 +30,29 @@ export const useAppsRoomActions = () => { groups: ['group', 'channel', 'live', 'team', 'direct', 'direct_multiple'], // Filters were applied in the applyButtonFilters function // if the code made it this far, the button should be shown - action: () => - void actionManager.triggerActionButtonAction({ - rid: room._id, - actionId: action.actionId, - appId: action.appId, - payload: { context: action.context }, - }), + action: () => { + void actionManager + .emitInteraction(action.appId, { + type: 'actionButton', + actionId: action.actionId, + rid: room._id, + payload: { context: action.context }, + }) + .catch(async (reason) => { + if (reason instanceof UiKitTriggerTimeoutError) { + dispatchToastMessage({ + type: 'error', + message: t('UIKit_Interaction_Timeout'), + }); + return; + } + + return reason; + }); + }, type: 'apps', }), ) ?? [], - [actionManager, applyButtonFilters, result.data, room._id], + [actionManager, applyButtonFilters, dispatchToastMessage, result.data, room._id, t], ); }; diff --git a/apps/meteor/ee/app/license/server/maxSeatsBanners.ts b/apps/meteor/ee/app/license/server/maxSeatsBanners.ts index 1aefb7848a42..b5aba719f29d 100644 --- a/apps/meteor/ee/app/license/server/maxSeatsBanners.ts +++ b/apps/meteor/ee/app/license/server/maxSeatsBanners.ts @@ -1,5 +1,3 @@ -import { BlockType } from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks'; -import { TextObjectType } from '@rocket.chat/apps-engine/definition/uikit/blocks/Objects'; import { Banner } from '@rocket.chat/core-services'; import type { IBanner } from '@rocket.chat/core-typings'; import { BannerPlatform } from '@rocket.chat/core-typings'; @@ -21,15 +19,14 @@ const makeWarningBanner = (seats: number): IBanner => ({ appId: 'banner-core', blocks: [ { - type: BlockType.SECTION, + type: 'section', blockId: 'attention', text: { - type: TextObjectType.MARKDOWN, + type: 'mrkdwn', text: i18n.t('Close_to_seat_limit_banner_warning', { seats, url: Meteor.absoluteUrl('/requestSeats'), }), - emoji: false, }, }, ], @@ -56,14 +53,13 @@ const makeDangerBanner = (): IBanner => ({ appId: 'banner-core', blocks: [ { - type: BlockType.SECTION, + type: 'section', blockId: 'attention', text: { - type: TextObjectType.MARKDOWN, + type: 'mrkdwn', text: i18n.t('Reached_seat_limit_banner_warning', { url: Meteor.absoluteUrl('/requestSeats'), }), - emoji: false, }, }, ], diff --git a/apps/meteor/ee/client/apps/gameCenter/GameCenter.tsx b/apps/meteor/ee/client/apps/gameCenter/GameCenter.tsx index c5377fdc30a0..75f4882ce747 100644 --- a/apps/meteor/ee/client/apps/gameCenter/GameCenter.tsx +++ b/apps/meteor/ee/client/apps/gameCenter/GameCenter.tsx @@ -1,8 +1,9 @@ import type { IExternalComponent } from '@rocket.chat/apps-engine/definition/externalComponent'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import React, { useState } from 'react'; -import type { ReactElement, SyntheticEvent } from 'react'; +import type { ReactElement } from 'react'; +import { preventSyntheticEvent } from '../../../../client/lib/utils/preventSyntheticEvent'; import { useRoomToolbox } from '../../../../client/views/room/contexts/RoomToolboxContext'; import GameCenterContainer from './GameCenterContainer'; import GameCenterList from './GameCenterList'; @@ -10,14 +11,6 @@ import { useExternalComponentsQuery } from './hooks/useExternalComponentsQuery'; export type IGame = IExternalComponent; -const prevent = (e: SyntheticEvent): void => { - if (e) { - (e.nativeEvent || e).stopImmediatePropagation(); - e.stopPropagation(); - e.preventDefault(); - } -}; - const GameCenter = (): ReactElement => { const [openedGame, setOpenedGame] = useState(); @@ -26,13 +19,13 @@ const GameCenter = (): ReactElement => { const result = useExternalComponentsQuery(); const handleClose = useMutableCallback((e) => { - prevent(e); + preventSyntheticEvent(e); closeTab(); }); const handleBack = useMutableCallback((e) => { setOpenedGame(undefined); - prevent(e); + preventSyntheticEvent(e); }); return ( diff --git a/apps/meteor/ee/server/apps/communication/uikit.ts b/apps/meteor/ee/server/apps/communication/uikit.ts index a7f84eab619b..61dee0a1857f 100644 --- a/apps/meteor/ee/server/apps/communication/uikit.ts +++ b/apps/meteor/ee/server/apps/communication/uikit.ts @@ -1,6 +1,6 @@ -import { AppInterface } from '@rocket.chat/apps-engine/definition/metadata'; -import { UIKitIncomingInteractionType } from '@rocket.chat/apps-engine/definition/uikit'; +import type { UiKitCoreAppPayload } from '@rocket.chat/core-services'; import { UiKitCoreApp } from '@rocket.chat/core-services'; +import type { OperationParams, UrlParams } from '@rocket.chat/rest-typings'; import cors from 'cors'; import type { Request, Response } from 'express'; import express from 'express'; @@ -91,41 +91,58 @@ const corsOptions: cors.CorsOptions = { apiServer.use('/api/apps/ui.interaction/', cors(corsOptions), router); // didn't have the rateLimiter option -const getPayloadForType = (type: UIKitIncomingInteractionType, req: Request) => { - if (type === UIKitIncomingInteractionType.BLOCK) { - const { type, actionId, triggerId, mid, rid, payload, container } = req.body; +type UiKitUserInteractionRequest = Request< + UrlParams<'/apps/ui.interaction/:id'>, + any, + OperationParams<'POST', '/apps/ui.interaction/:id'> & { + visitor?: { + id: string; + username: string; + name?: string; + department?: string; + updatedAt?: Date; + token: string; + phone?: { phoneNumber: string }[] | null; + visitorEmails?: { address: string }[]; + livechatData?: Record; + status?: 'online' | 'away' | 'offline' | 'busy' | 'disabled'; + }; + } +>; - const { visitor } = req.body; - const { user } = req; +const getCoreAppPayload = (req: UiKitUserInteractionRequest): UiKitCoreAppPayload => { + const { id: appId } = req.params; - const room = rid; // orch.getConverters().get('rooms').convertById(rid); - const message = mid; + if (req.body.type === 'blockAction') { + const { user } = req; + const { type, actionId, triggerId, payload, container, visitor } = req.body; + const message = 'mid' in req.body ? req.body.mid : undefined; + const room = 'rid' in req.body ? req.body.rid : undefined; return { + appId, type, - container, actionId, - message, triggerId, + container, + message, payload, user, visitor, room, - } as const; + }; } - if (type === UIKitIncomingInteractionType.VIEW_CLOSED) { + if (req.body.type === 'viewClosed') { + const { user } = req; const { type, - actionId, payload: { view, isCleared }, } = req.body; - const { user } = req; - return { + appId, type, - actionId, user, payload: { view, @@ -134,12 +151,12 @@ const getPayloadForType = (type: UIKitIncomingInteractionType, req: Request) => }; } - if (type === UIKitIncomingInteractionType.VIEW_SUBMIT) { - const { type, actionId, triggerId, payload } = req.body; - + if (req.body.type === 'viewSubmit') { const { user } = req; + const { type, actionId, triggerId, payload } = req.body; return { + appId, type, actionId, triggerId, @@ -151,24 +168,18 @@ const getPayloadForType = (type: UIKitIncomingInteractionType, req: Request) => throw new Error('Type not supported'); }; -router.post('/:appId', async (req, res, next) => { - const { appId } = req.params; +router.post('/:id', async (req: UiKitUserInteractionRequest, res, next) => { + const { id: appId } = req.params; - const isCore = await UiKitCoreApp.isRegistered(appId); - if (!isCore) { + const isCoreApp = await UiKitCoreApp.isRegistered(appId); + if (!isCoreApp) { return next(); } - // eslint-disable-next-line prefer-destructuring - const type: UIKitIncomingInteractionType = req.body.type; - try { - const payload = { - ...getPayloadForType(type, req), - appId, - }; + const payload = getCoreAppPayload(req); - const result = await (UiKitCoreApp as any)[type](payload); // TO-DO: fix type + const result = await UiKitCoreApp[payload.type](payload); // Using ?? to always send something in the response, even if the app had no result. res.send(result ?? {}); @@ -178,16 +189,24 @@ router.post('/:appId', async (req, res, next) => { } }); -const appsRoutes = - (orch: AppServerOrchestrator) => - async (req: Request, res: Response): Promise => { - const { appId } = req.params; +export class AppUIKitInteractionApi { + orch: AppServerOrchestrator; + + constructor(orch: AppServerOrchestrator) { + this.orch = orch; + + router.post('/:id', this.routeHandler); + } - const { type } = req.body; + private routeHandler = async (req: UiKitUserInteractionRequest, res: Response): Promise => { + const { orch } = this; + const { id: appId } = req.params; - switch (type) { - case UIKitIncomingInteractionType.BLOCK: { - const { type, actionId, triggerId, mid, rid, payload, container } = req.body; + switch (req.body.type) { + case 'blockAction': { + const { type, actionId, triggerId, payload, container } = req.body; + const mid = 'mid' in req.body ? req.body.mid : undefined; + const rid = 'rid' in req.body ? req.body.rid : undefined; const { visitor } = req.body; const room = await orch.getConverters()?.get('rooms').convertById(rid); @@ -208,7 +227,7 @@ const appsRoutes = }; try { - const eventInterface = !visitor ? AppInterface.IUIKitInteractionHandler : AppInterface.IUIKitLivechatInteractionHandler; + const eventInterface = !visitor ? 'IUIKitInteractionHandler' : 'IUIKitLivechatInteractionHandler'; const result = await orch.triggerEvent(eventInterface, action); @@ -220,10 +239,9 @@ const appsRoutes = break; } - case UIKitIncomingInteractionType.VIEW_CLOSED: { + case 'viewClosed': { const { type, - actionId, payload: { view, isCleared }, } = req.body; @@ -232,7 +250,6 @@ const appsRoutes = const action = { type, appId, - actionId, user, payload: { view, @@ -251,7 +268,7 @@ const appsRoutes = break; } - case UIKitIncomingInteractionType.VIEW_SUBMIT: { + case 'viewSubmit': { const { type, actionId, triggerId, payload } = req.body; const user = orch.getConverters()?.get('users').convertToApp(req.user); @@ -276,7 +293,7 @@ const appsRoutes = break; } - case UIKitIncomingInteractionType.ACTION_BUTTON: { + case 'actionButton': { const { type, actionId, @@ -302,7 +319,7 @@ const appsRoutes = tmid, payload: { context, - ...(msgText && { message: msgText }), + ...(msgText ? { message: msgText } : {}), }, }; @@ -324,13 +341,4 @@ const appsRoutes = // TODO: validate payloads per type }; - -export class AppUIKitInteractionApi { - orch: AppServerOrchestrator; - - constructor(orch: AppServerOrchestrator) { - this.orch = orch; - - router.post('/:appId', appsRoutes(orch)); - } } diff --git a/apps/meteor/server/modules/core-apps/banner.module.ts b/apps/meteor/server/modules/core-apps/banner.module.ts index bc850fea2078..fac891e5ea73 100644 --- a/apps/meteor/server/modules/core-apps/banner.module.ts +++ b/apps/meteor/server/modules/core-apps/banner.module.ts @@ -1,18 +1,24 @@ import { Banner } from '@rocket.chat/core-services'; -import type { IUiKitCoreApp } from '@rocket.chat/core-services'; +import type { IUiKitCoreApp, UiKitCoreAppPayload } from '@rocket.chat/core-services'; export class BannerModule implements IUiKitCoreApp { appId = 'banner-core'; // when banner view is closed we need to dissmiss that banner for that user - async viewClosed(payload: any): Promise { + async viewClosed(payload: UiKitCoreAppPayload) { const { - payload: { - view: { viewId: bannerId }, - }, - user: { _id: userId }, + payload: { view: { viewId: bannerId } = {} }, + user: { _id: userId } = {}, } = payload; + if (!userId) { + throw new Error('invalid user'); + } + + if (!bannerId) { + throw new Error('invalid banner'); + } + return Banner.dismiss(userId, bannerId); } } diff --git a/apps/meteor/server/modules/core-apps/nps.module.ts b/apps/meteor/server/modules/core-apps/nps.module.ts index 68ebeffd97c2..6e8965122df3 100644 --- a/apps/meteor/server/modules/core-apps/nps.module.ts +++ b/apps/meteor/server/modules/core-apps/nps.module.ts @@ -1,4 +1,4 @@ -import type { IUiKitCoreApp } from '@rocket.chat/core-services'; +import type { IUiKitCoreApp, UiKitCoreAppPayload } from '@rocket.chat/core-services'; import { Banner, NPS } from '@rocket.chat/core-services'; import { createModal } from './nps/createModal'; @@ -6,15 +6,19 @@ import { createModal } from './nps/createModal'; export class Nps implements IUiKitCoreApp { appId = 'nps-core'; - async blockAction(payload: any): Promise { + async blockAction(payload: UiKitCoreAppPayload) { const { triggerId, actionId, - container: { id: viewId }, + container: { id: viewId } = {}, payload: { value: score, blockId: npsId }, user, } = payload; + if (!viewId || !triggerId || !user || !npsId) { + throw new Error('Invalid payload'); + } + const bannerId = viewId.replace(`${npsId}-`, ''); return createModal({ @@ -23,13 +27,13 @@ export class Nps implements IUiKitCoreApp { appId: this.appId, npsId, triggerId, - score, + score: String(score), user, }); } - async viewSubmit(payload: any): Promise { - if (!payload.payload?.view?.state) { + async viewSubmit(payload: UiKitCoreAppPayload) { + if (!payload.payload?.view?.state || !payload.payload?.view?.id) { throw new Error('Invalid payload'); } @@ -37,7 +41,7 @@ export class Nps implements IUiKitCoreApp { payload: { view: { state, id: viewId }, }, - user: { _id: userId, roles }, + user: { _id: userId, roles } = {}, } = payload; const [npsId] = Object.keys(state); @@ -51,11 +55,15 @@ export class Nps implements IUiKitCoreApp { await NPS.vote({ npsId, userId, - comment, + comment: String(comment), roles, - score, + score: Number(score), }); + if (!userId) { + throw new Error('invalid user'); + } + await Banner.dismiss(userId, bannerId); return true; diff --git a/apps/meteor/server/modules/core-apps/videoconf.module.ts b/apps/meteor/server/modules/core-apps/videoconf.module.ts index b0425f6ffd55..694a0fac9b8e 100644 --- a/apps/meteor/server/modules/core-apps/videoconf.module.ts +++ b/apps/meteor/server/modules/core-apps/videoconf.module.ts @@ -1,4 +1,4 @@ -import type { IUiKitCoreApp } from '@rocket.chat/core-services'; +import type { IUiKitCoreApp, UiKitCoreAppPayload } from '@rocket.chat/core-services'; import { VideoConf } from '@rocket.chat/core-services'; import { i18n } from '../../lib/i18n'; @@ -6,14 +6,18 @@ import { i18n } from '../../lib/i18n'; export class VideoConfModule implements IUiKitCoreApp { appId = 'videoconf-core'; - async blockAction(payload: any): Promise { + async blockAction(payload: UiKitCoreAppPayload) { const { triggerId, actionId, payload: { blockId: callId }, - user: { _id: userId }, + user: { _id: userId } = {}, } = payload; + if (!callId) { + throw new Error('invalid call'); + } + if (actionId === 'join') { await VideoConf.join(userId, callId, {}); } diff --git a/apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts b/apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts index 02a3c29eedf3..8e4c06941c81 100644 --- a/apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts +++ b/apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts @@ -1,5 +1,5 @@ import { Banner } from '@rocket.chat/core-services'; -import type { UiKitBannerPayload, IBanner, BannerPlatform } from '@rocket.chat/core-typings'; +import type { UiKit, IBanner, BannerPlatform } from '@rocket.chat/core-typings'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { getWorkspaceAccessToken } from '../../../app/cloud/server'; @@ -10,7 +10,7 @@ type NpsSurveyData = { id: string; platform: BannerPlatform[]; roles: string[]; - survey: UiKitBannerPayload; + survey: UiKit.BannerView; createdAt: Date; startAt: Date; expireAt: Date; diff --git a/apps/meteor/server/services/nps/notification.ts b/apps/meteor/server/services/nps/notification.ts index 91ed3c7d2671..692b9bc6291f 100644 --- a/apps/meteor/server/services/nps/notification.ts +++ b/apps/meteor/server/services/nps/notification.ts @@ -1,5 +1,3 @@ -import { BlockType } from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks'; -import { TextObjectType } from '@rocket.chat/apps-engine/definition/uikit/blocks/Objects'; import type { IBanner } from '@rocket.chat/core-typings'; import { BannerPlatform } from '@rocket.chat/core-typings'; import moment from 'moment'; @@ -27,10 +25,10 @@ export const getBannerForAdmins = (expireAt: Date): Omit => { appId: '', blocks: [ { - type: BlockType.SECTION, + type: 'section', blockId: 'attention', text: { - type: TextObjectType.PLAINTEXT, + type: 'plain_text', text: i18n.t('NPS_survey_is_scheduled_to-run-at__date__for_all_users', { date: moment(expireAt).format('YYYY-MM-DD'), lng, diff --git a/apps/meteor/server/services/startup.ts b/apps/meteor/server/services/startup.ts index 968415620558..28ab5a35b553 100644 --- a/apps/meteor/server/services/startup.ts +++ b/apps/meteor/server/services/startup.ts @@ -25,7 +25,7 @@ import { SAUMonitorService } from './sauMonitor/service'; import { SettingsService } from './settings/service'; import { TeamService } from './team/service'; import { TranslationService } from './translation/service'; -import { UiKitCoreApp } from './uikit-core-app/service'; +import { UiKitCoreAppService } from './uikit-core-app/service'; import { UploadService } from './upload/service'; import { VideoConfService } from './video-conference/service'; import { VoipService } from './voip/service'; @@ -47,7 +47,7 @@ api.registerService(new VoipService(db)); api.registerService(new OmnichannelService()); api.registerService(new OmnichannelVoipService()); api.registerService(new TeamService()); -api.registerService(new UiKitCoreApp()); +api.registerService(new UiKitCoreAppService()); api.registerService(new PushService()); api.registerService(new DeviceManagementService()); api.registerService(new VideoConfService()); diff --git a/apps/meteor/server/services/uikit-core-app/service.ts b/apps/meteor/server/services/uikit-core-app/service.ts index a9eddf69ce81..a842a4854c6a 100644 --- a/apps/meteor/server/services/uikit-core-app/service.ts +++ b/apps/meteor/server/services/uikit-core-app/service.ts @@ -1,9 +1,9 @@ import { ServiceClassInternal } from '@rocket.chat/core-services'; -import type { IUiKitCoreApp, IUiKitCoreAppService } from '@rocket.chat/core-services'; +import type { IUiKitCoreApp, IUiKitCoreAppService, UiKitCoreAppPayload } from '@rocket.chat/core-services'; -const registeredApps = new Map(); +const registeredApps = new Map(); -const getAppModule = (appId: string): any => { +const getAppModule = (appId: string) => { const module = registeredApps.get(appId); if (typeof module === 'undefined') { @@ -17,14 +17,14 @@ export const registerCoreApp = (module: IUiKitCoreApp): void => { registeredApps.set(module.appId, module); }; -export class UiKitCoreApp extends ServiceClassInternal implements IUiKitCoreAppService { +export class UiKitCoreAppService extends ServiceClassInternal implements IUiKitCoreAppService { protected name = 'uikit-core-app'; async isRegistered(appId: string): Promise { return registeredApps.has(appId); } - async blockAction(payload: any): Promise { + async blockAction(payload: UiKitCoreAppPayload) { const { appId } = payload; const service = getAppModule(appId); @@ -35,7 +35,7 @@ export class UiKitCoreApp extends ServiceClassInternal implements IUiKitCoreAppS return service.blockAction?.(payload); } - async viewClosed(payload: any): Promise { + async viewClosed(payload: UiKitCoreAppPayload) { const { appId } = payload; const service = getAppModule(appId); @@ -46,7 +46,7 @@ export class UiKitCoreApp extends ServiceClassInternal implements IUiKitCoreAppS return service.viewClosed?.(payload); } - async viewSubmit(payload: any): Promise { + async viewSubmit(payload: UiKitCoreAppPayload) { const { appId } = payload; const service = getAppModule(appId); diff --git a/apps/meteor/server/services/video-conference/service.ts b/apps/meteor/server/services/video-conference/service.ts index c9079b0a2bfb..818280fd4d31 100644 --- a/apps/meteor/server/services/video-conference/service.ts +++ b/apps/meteor/server/services/video-conference/service.ts @@ -1,4 +1,3 @@ -import type { IBlock } from '@rocket.chat/apps-engine/definition/uikit'; import type { AppVideoConfProviderManager } from '@rocket.chat/apps-engine/server/managers'; import type { IVideoConfService, VideoConferenceJoinOptions } from '@rocket.chat/core-services'; import { api, ServiceClassInternal } from '@rocket.chat/core-services'; @@ -20,6 +19,7 @@ import type { VideoConferenceCapabilities, VideoConferenceCreateData, Optional, + UiKit, } from '@rocket.chat/core-typings'; import { VideoConferenceStatus, @@ -136,7 +136,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf return this.joinCall(call, user || undefined, options); } - public async getInfo(callId: VideoConference['_id'], uid: IUser['_id'] | undefined): Promise { + public async getInfo(callId: VideoConference['_id'], uid: IUser['_id'] | undefined): Promise { const call = await VideoConferenceModel.findOneById(callId); if (!call) { throw new Error('invalid-call'); @@ -162,7 +162,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf }); if (blocks?.length) { - return blocks; + return blocks as UiKit.LayoutBlock[]; } return [ @@ -173,7 +173,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf type: 'mrkdwn', text: `**${i18n.t('Video_Conference_Url')}**: ${call.url}`, }, - } as IBlock, + }, ]; } diff --git a/ee/packages/ddp-client/src/types/streams.ts b/ee/packages/ddp-client/src/types/streams.ts index a32dec470564..9010517faf7a 100644 --- a/ee/packages/ddp-client/src/types/streams.ts +++ b/ee/packages/ddp-client/src/types/streams.ts @@ -1,6 +1,5 @@ import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import type { ISetting as AppsSetting } from '@rocket.chat/apps-engine/definition/settings'; -import type { IUIKitInteraction } from '@rocket.chat/apps-engine/definition/uikit'; import type { IMessage, IRoom, @@ -24,6 +23,7 @@ import type { ILivechatAgent, IImportProgress, IBanner, + UiKit, } from '@rocket.chat/core-typings'; type ClientAction = 'inserted' | 'updated' | 'removed' | 'changed'; @@ -148,7 +148,7 @@ export interface StreamerEvents { { key: `${string}/notification`; args: [INotificationDesktop] }, { key: `${string}/voip.events`; args: [VoipEventDataSignature] }, { key: `${string}/call.hangup`; args: [{ roomId: string }] }, - { key: `${string}/uiInteraction`; args: [IUIKitInteraction] }, + { key: `${string}/uiInteraction`; args: [UiKit.ServerInteraction] }, { key: `${string}/video-conference`; args: [{ action: string; params: { callId: VideoConference['_id']; uid: IUser['_id']; rid: IRoom['_id'] } }]; diff --git a/package.json b/package.json index 962e42d48c6e..236a551c5e5e 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@types/chart.js": "^2.9.37", "@types/js-yaml": "^4.0.5", "husky": "^7.0.4", - "turbo": "~1.10.14" + "turbo": "~1.10.15" }, "workspaces": [ "apps/*", diff --git a/packages/core-services/src/Events.ts b/packages/core-services/src/Events.ts index e2a7f624d8df..2aa39588d2f0 100644 --- a/packages/core-services/src/Events.ts +++ b/packages/core-services/src/Events.ts @@ -1,6 +1,5 @@ import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import type { ISetting as AppsSetting } from '@rocket.chat/apps-engine/definition/settings'; -import type { IUIKitInteraction } from '@rocket.chat/apps-engine/definition/uikit'; import type { IEmailInbox, IEmoji, @@ -33,6 +32,7 @@ import type { ILivechatAgent, IBanner, ILivechatVisitor, + UiKit, } from '@rocket.chat/core-typings'; import type { AutoUpdateRecord } from './types/IMeteor'; @@ -59,7 +59,7 @@ export type EventSignatures = { 'message'(data: { action: string; message: IMessage }): void; 'meteor.clientVersionUpdated'(data: AutoUpdateRecord): void; 'notify.desktop'(uid: string, data: INotificationDesktop): void; - 'notify.uiInteraction'(uid: string, data: IUIKitInteraction): void; + 'notify.uiInteraction'(uid: string, data: UiKit.ServerInteraction): void; 'notify.updateInvites'(uid: string, data: { invite: Omit }): void; 'notify.ephemeralMessage'(uid: string, rid: string, message: AtLeast): void; 'notify.webdav'( diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index def7622c9881..d3cc778e5a22 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -41,7 +41,7 @@ import type { } from './types/ITeamService'; import type { ITelemetryEvent, TelemetryMap, TelemetryEvents } from './types/ITelemetryEvent'; import type { ITranslationService } from './types/ITranslationService'; -import type { IUiKitCoreApp, IUiKitCoreAppService } from './types/IUiKitCoreApp'; +import type { UiKitCoreAppPayload, IUiKitCoreApp, IUiKitCoreAppService } from './types/IUiKitCoreApp'; import type { ISendFileLivechatMessageParams, ISendFileMessageParams, IUploadFileParams, IUploadService } from './types/IUploadService'; import type { IVideoConfService, VideoConferenceJoinOptions } from './types/IVideoConfService'; import type { IVoipService } from './types/IVoipService'; @@ -94,6 +94,7 @@ export { ITeamService, ITeamUpdateData, ITelemetryEvent, + UiKitCoreAppPayload, IUiKitCoreApp, IUiKitCoreAppService, IVideoConfService, diff --git a/packages/core-services/src/types/INPSService.ts b/packages/core-services/src/types/INPSService.ts index 4590a2910e8c..eaf54f6c6133 100644 --- a/packages/core-services/src/types/INPSService.ts +++ b/packages/core-services/src/types/INPSService.ts @@ -1,9 +1,9 @@ import type { IUser, IRole } from '@rocket.chat/core-typings'; export type NPSVotePayload = { - userId: string; + userId: string | undefined; npsId: string; - roles: IRole['_id'][]; + roles?: IRole['_id'][]; score: number; comment: string; }; diff --git a/packages/core-services/src/types/IUiKitCoreApp.ts b/packages/core-services/src/types/IUiKitCoreApp.ts index 92c7b7bd738e..98799918e594 100644 --- a/packages/core-services/src/types/IUiKitCoreApp.ts +++ b/packages/core-services/src/types/IUiKitCoreApp.ts @@ -1,16 +1,55 @@ +import type { IUser } from '@rocket.chat/core-typings'; + import type { IServiceClass } from './ServiceClass'; +export type UiKitCoreAppPayload = { + appId: string; + type: 'blockAction' | 'viewClosed' | 'viewSubmit'; + actionId?: string; + triggerId?: string; + container?: { + id: string; + [key: string]: unknown; + }; + message?: unknown; + payload: { + blockId?: string; + value?: unknown; + view?: { + viewId?: string; + id?: string; + state?: { [blockId: string]: { [key: string]: unknown } }; + [key: string]: unknown; + }; + isCleared?: unknown; + }; + user?: IUser; + visitor?: { + id: string; + username: string; + name?: string; + department?: string; + updatedAt?: Date; + token: string; + phone?: { phoneNumber: string }[] | null; + visitorEmails?: { address: string }[]; + livechatData?: Record; + status?: 'online' | 'away' | 'offline' | 'busy' | 'disabled'; + }; + room?: unknown; +}; + export interface IUiKitCoreApp { appId: string; - blockAction?(payload: any): Promise; - viewClosed?(payload: any): Promise; - viewSubmit?(payload: any): Promise; + blockAction?(payload: UiKitCoreAppPayload): Promise; + viewClosed?(payload: UiKitCoreAppPayload): Promise; + viewSubmit?(payload: UiKitCoreAppPayload): Promise; } export interface IUiKitCoreAppService extends IServiceClass { isRegistered(appId: string): Promise; - blockAction(payload: any): Promise; - viewClosed(payload: any): Promise; - viewSubmit(payload: any): Promise; + blockAction(payload: UiKitCoreAppPayload): Promise; + viewClosed(payload: UiKitCoreAppPayload): Promise; + viewSubmit(payload: UiKitCoreAppPayload): Promise; } diff --git a/packages/core-services/src/types/IVideoConfService.ts b/packages/core-services/src/types/IVideoConfService.ts index d545365b452a..09e336a51623 100644 --- a/packages/core-services/src/types/IVideoConfService.ts +++ b/packages/core-services/src/types/IVideoConfService.ts @@ -1,8 +1,8 @@ -import type { IBlock } from '@rocket.chat/apps-engine/definition/uikit'; import type { IRoom, IStats, IUser, + UiKit, VideoConference, VideoConferenceCapabilities, VideoConferenceCreateData, @@ -19,7 +19,7 @@ export interface IVideoConfService { create(data: VideoConferenceCreateData, useAppUser?: boolean): Promise; start(caller: IUser['_id'], rid: string, options: { title?: string; allowRinging?: boolean }): Promise; join(uid: IUser['_id'] | undefined, callId: VideoConference['_id'], options: VideoConferenceJoinOptions): Promise; - getInfo(callId: VideoConference['_id'], uid: IUser['_id'] | undefined): Promise; + getInfo(callId: VideoConference['_id'], uid: IUser['_id'] | undefined): Promise; cancel(uid: IUser['_id'], callId: VideoConference['_id']): Promise; get(callId: VideoConference['_id']): Promise | null>; getUnfiltered(callId: VideoConference['_id']): Promise; diff --git a/packages/core-typings/src/IBanner.ts b/packages/core-typings/src/IBanner.ts index 29867cdfb6c8..275c3353aa1f 100644 --- a/packages/core-typings/src/IBanner.ts +++ b/packages/core-typings/src/IBanner.ts @@ -1,6 +1,6 @@ import type { IRocketChatRecord } from './IRocketChatRecord'; import type { IUser } from './IUser'; -import type { UiKitBannerPayload } from './UIKit'; +import type * as UiKit from './uikit'; export enum BannerPlatform { Web = 'web', @@ -13,7 +13,7 @@ export interface IBanner extends IRocketChatRecord { roles?: string[]; // only show the banner to this roles createdBy: Pick; createdAt: Date; - view: UiKitBannerPayload; + view: UiKit.BannerView; active?: boolean; inactivedAt?: Date; snapshot?: string; diff --git a/packages/core-typings/src/INps.ts b/packages/core-typings/src/INps.ts index e89796d9d9a4..12b3a1a15d89 100644 --- a/packages/core-typings/src/INps.ts +++ b/packages/core-typings/src/INps.ts @@ -27,7 +27,7 @@ export interface INpsVote extends IRocketChatRecord { npsId: INps['_id']; ts: Date; identifier: string; // voter identifier - roles: IUser['roles']; // voter roles + roles?: IUser['roles']; // voter roles score: number; comment: string; status: INpsVoteStatus; diff --git a/packages/core-typings/src/Serialized.ts b/packages/core-typings/src/Serialized.ts index c84077610ee8..94f79cb64d06 100644 --- a/packages/core-typings/src/Serialized.ts +++ b/packages/core-typings/src/Serialized.ts @@ -1,9 +1,26 @@ -export type Serialized = T extends Date - ? Exclude | string - : T extends boolean | number | string | null | undefined +/* eslint-disable @typescript-eslint/ban-types */ + +type SerializablePrimitive = boolean | number | string | null; + +type UnserializablePrimitive = Function | bigint | symbol | undefined; + +type CustomSerializable = { + toJSON(key: string): T; +}; + +/** + * The type of a value that was serialized via `JSON.stringify` and then deserialized via `JSON.parse`. + */ +export type Serialized = T extends CustomSerializable + ? Serialized + : T extends [any, ...any] // is T a tuple? + ? { [K in keyof T]: T extends UnserializablePrimitive ? null : Serialized } + : T extends any[] + ? Serialized[] + : T extends object + ? { [K in keyof T]: Serialized } + : T extends SerializablePrimitive ? T - : T extends {} - ? { - [K in keyof T]: Serialized; - } + : T extends UnserializablePrimitive + ? undefined : null; diff --git a/packages/core-typings/src/UIKit.ts b/packages/core-typings/src/UIKit.ts deleted file mode 100644 index 19cf46f82b92..000000000000 --- a/packages/core-typings/src/UIKit.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { UIKitInteractionType as UIKitInteractionTypeApi } from '@rocket.chat/apps-engine/definition/uikit'; -import type { - IDividerBlock, - ISectionBlock, - IActionsBlock, - IContextBlock, - IInputBlock, -} from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks'; - -enum UIKitInteractionTypeExtended { - BANNER_OPEN = 'banner.open', - BANNER_UPDATE = 'banner.update', - BANNER_CLOSE = 'banner.close', -} - -export type UIKitInteractionType = UIKitInteractionTypeApi | UIKitInteractionTypeExtended; - -export const UIKitInteractionTypes = { - ...UIKitInteractionTypeApi, - ...UIKitInteractionTypeExtended, -}; - -export type UiKitPayload = { - viewId: string; - appId: string; - blocks: (IDividerBlock | ISectionBlock | IActionsBlock | IContextBlock | IInputBlock)[]; -}; - -export type UiKitBannerPayload = UiKitPayload & { - inline?: boolean; - variant?: 'neutral' | 'info' | 'success' | 'warning' | 'danger'; - icon?: string; - title?: string; -}; - -export type UIKitUserInteraction = { - type: UIKitInteractionType; -} & UiKitPayload; - -export type UiKitBannerProps = { - payload: UiKitBannerPayload; -}; - -export type UIKitUserInteractionResult = UIKitUserInteractionResultError | UIKitUserInteraction; - -type UIKitUserInteractionResultError = UIKitUserInteraction & { - type: UIKitInteractionTypeApi.ERRORS; - errors?: Array<{ [key: string]: string }>; -}; - -export const isErrorType = (result: UIKitUserInteractionResult): result is UIKitUserInteractionResultError => - result.type === UIKitInteractionTypeApi.ERRORS; - -export type UIKitActionEvent = { - blockId: string; - value?: unknown; - appId: string; - actionId: string; - viewId: string; -}; diff --git a/packages/core-typings/src/cloud/Announcement.ts b/packages/core-typings/src/cloud/Announcement.ts index 3d891daf132f..7c9541efe75a 100644 --- a/packages/core-typings/src/cloud/Announcement.ts +++ b/packages/core-typings/src/cloud/Announcement.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type { IRocketChatRecord } from '../IRocketChatRecord'; -import { type UiKitPayload } from '../UIKit'; +import type * as UiKit from '../uikit'; type TargetPlatform = 'web' | 'mobile'; @@ -23,6 +23,6 @@ export interface Announcement extends IRocketChatRecord { createdBy: Creator; createdAt: Date; dictionary?: Dictionary; - view: UiKitPayload; + view: UiKit.View; surface: 'banner' | 'modal'; } diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index de36606e7f90..6411390f0fe9 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -4,7 +4,6 @@ export * from './FeaturedApps'; export * from './AppRequests'; export * from './MarketplaceRest'; export * from './IRoom'; -export * from './UIKit'; export * from './IMessage'; export * from './federation'; export * from './Serialized'; @@ -136,3 +135,5 @@ export * from './IModerationReport'; export * from './CustomFieldMetadata'; export * as Cloud from './cloud'; + +export * as UiKit from './uikit'; diff --git a/packages/core-typings/src/uikit/BannerView.ts b/packages/core-typings/src/uikit/BannerView.ts new file mode 100644 index 000000000000..f6914f75a6af --- /dev/null +++ b/packages/core-typings/src/uikit/BannerView.ts @@ -0,0 +1,16 @@ +import type { Keys as IconName } from '@rocket.chat/icons'; +import type { BannerSurfaceLayout } from '@rocket.chat/ui-kit'; + +import type { View } from './View'; + +/** + * A view that is displayed as a banner. + */ +export type BannerView = View & { + viewId: string; + inline?: boolean; + variant?: 'neutral' | 'info' | 'success' | 'warning' | 'danger'; + icon?: IconName; + title?: string; // TODO: change to plain_text block in the future + blocks: BannerSurfaceLayout; +}; diff --git a/packages/core-typings/src/uikit/ContextualBarView.ts b/packages/core-typings/src/uikit/ContextualBarView.ts new file mode 100644 index 000000000000..ab480be19b77 --- /dev/null +++ b/packages/core-typings/src/uikit/ContextualBarView.ts @@ -0,0 +1,14 @@ +import type { ButtonElement, ContextualBarSurfaceLayout, TextObject } from '@rocket.chat/ui-kit'; + +import type { View } from './View'; + +/** + * A view that is displayed as a contextual bar. + */ +export type ContextualBarView = View & { + id: string; + title: TextObject; + close?: ButtonElement; + submit?: ButtonElement; + blocks: ContextualBarSurfaceLayout; +}; diff --git a/packages/core-typings/src/uikit/ModalView.ts b/packages/core-typings/src/uikit/ModalView.ts new file mode 100644 index 000000000000..2e2fc12befe8 --- /dev/null +++ b/packages/core-typings/src/uikit/ModalView.ts @@ -0,0 +1,15 @@ +import type { ButtonElement, ModalSurfaceLayout, TextObject } from '@rocket.chat/ui-kit'; + +import type { View } from './View'; + +/** + * A view that is displayed as a modal dialog. + */ +export type ModalView = View & { + id: string; + showIcon?: boolean; + title: TextObject; + close?: ButtonElement; + submit?: ButtonElement; + blocks: ModalSurfaceLayout; +}; diff --git a/packages/core-typings/src/uikit/ServerInteraction.ts b/packages/core-typings/src/uikit/ServerInteraction.ts new file mode 100644 index 000000000000..a5b8aabca26e --- /dev/null +++ b/packages/core-typings/src/uikit/ServerInteraction.ts @@ -0,0 +1,84 @@ +import type { BannerView } from './BannerView'; +import type { ContextualBarView } from './ContextualBarView'; +import type { ModalView } from './ModalView'; + +type OpenModalServerInteraction = { + type: 'modal.open'; + triggerId: string; + appId: string; + view: ModalView; +}; + +type UpdateModalServerInteraction = { + type: 'modal.update'; + triggerId: string; + appId: string; + view: ModalView; +}; + +type CloseModalServerInteraction = { + type: 'modal.close'; + triggerId: string; + appId: string; +}; + +type OpenBannerServerInteraction = { + type: 'banner.open'; + triggerId: string; + appId: string; +} & BannerView; + +type UpdateBannerServerInteraction = { + type: 'banner.update'; + triggerId: string; + appId: string; + view: BannerView; +}; + +type CloseBannerServerInteraction = { + type: 'banner.close'; + triggerId: string; + appId: string; + viewId: BannerView['viewId']; +}; + +type OpenContextualBarServerInteraction = { + type: 'contextual_bar.open'; + triggerId: string; + appId: string; + view: ContextualBarView; +}; + +type UpdateContextualBarServerInteraction = { + type: 'contextual_bar.update'; + triggerId: string; + appId: string; + view: ContextualBarView; +}; + +type CloseContextualBarServerInteraction = { + type: 'contextual_bar.close'; + triggerId: string; + appId: string; + view: ContextualBarView; +}; + +type ReportErrorsServerInteraction = { + type: 'errors'; + triggerId: string; + appId: string; + viewId: ModalView['id'] | BannerView['viewId'] | ContextualBarView['id']; + errors: { [field: string]: string }[]; +}; + +export type ServerInteraction = + | OpenModalServerInteraction + | UpdateModalServerInteraction + | CloseModalServerInteraction + | OpenBannerServerInteraction + | UpdateBannerServerInteraction + | CloseBannerServerInteraction + | OpenContextualBarServerInteraction + | UpdateContextualBarServerInteraction + | CloseContextualBarServerInteraction + | ReportErrorsServerInteraction; diff --git a/packages/core-typings/src/uikit/UserInteraction.ts b/packages/core-typings/src/uikit/UserInteraction.ts new file mode 100644 index 000000000000..3b65acb839f8 --- /dev/null +++ b/packages/core-typings/src/uikit/UserInteraction.ts @@ -0,0 +1,122 @@ +import type { IMessage } from '../IMessage'; +import type { IRoom } from '../IRoom'; +import type { View } from './View'; + +export type MessageBlockActionUserInteraction = { + type: 'blockAction'; + actionId: string; + payload: { + blockId: string; + value: unknown; + }; + container: { + type: 'message'; + id: IMessage['_id']; + }; + mid: IMessage['_id']; + tmid?: IMessage['_id']; + rid: IRoom['_id']; + triggerId: string; +}; + +export type ViewBlockActionUserInteraction = { + type: 'blockAction'; + actionId: string; + payload: { + blockId: string; + value: unknown; + }; + container: { + type: 'view'; + id: string; + }; + triggerId: string; +}; + +export type ViewClosedUserInteraction = { + type: 'viewClosed'; + payload: { + viewId: string; + view: View & { + id: string; + state: { [blockId: string]: { [key: string]: unknown } }; + }; + isCleared?: boolean; + }; + triggerId: string; +}; + +export type ViewSubmitUserInteraction = { + type: 'viewSubmit'; + actionId?: undefined; + payload: { + view: View & { + id: string; + state: { [blockId: string]: { [key: string]: unknown } }; + }; + }; + triggerId: string; + viewId: string; +}; + +export type MessageBoxActionButtonUserInteraction = { + type: 'actionButton'; + actionId: string; + payload: { + context: 'messageBoxAction'; + message: string; + }; + mid?: undefined; + tmid?: IMessage['_id']; + rid: IRoom['_id']; + triggerId: string; +}; + +export type UserDropdownActionButtonUserInteraction = { + type: 'actionButton'; + actionId: string; + payload: { + context: 'userDropdownAction'; + message?: undefined; + }; + mid?: undefined; + tmid?: undefined; + rid?: undefined; + triggerId: string; +}; + +export type MesssageActionButtonUserInteraction = { + type: 'actionButton'; + actionId: string; + payload: { + context: 'messageAction'; + message?: undefined; + }; + mid: IMessage['_id']; + tmid?: IMessage['_id']; + rid: IRoom['_id']; + triggerId: string; +}; + +export type RoomActionButtonUserInteraction = { + type: 'actionButton'; + actionId: string; + payload: { + context: 'roomAction'; + message?: undefined; + }; + mid?: undefined; + tmid?: undefined; + rid: IRoom['_id']; + triggerId: string; +}; + +export type UserInteraction = + | MessageBlockActionUserInteraction + | ViewBlockActionUserInteraction + | ViewClosedUserInteraction + | ViewSubmitUserInteraction + | MessageBoxActionButtonUserInteraction + | UserDropdownActionButtonUserInteraction + | MesssageActionButtonUserInteraction + | RoomActionButtonUserInteraction; diff --git a/packages/core-typings/src/uikit/View.ts b/packages/core-typings/src/uikit/View.ts new file mode 100644 index 000000000000..fe3b3a366635 --- /dev/null +++ b/packages/core-typings/src/uikit/View.ts @@ -0,0 +1,9 @@ +import type { LayoutBlock } from '@rocket.chat/ui-kit'; + +/** + * An instance of a UiKit surface and its metadata. + */ +export type View = { + appId: string; + blocks: LayoutBlock[]; +}; diff --git a/packages/core-typings/src/uikit/index.ts b/packages/core-typings/src/uikit/index.ts new file mode 100644 index 000000000000..61ab79621d1a --- /dev/null +++ b/packages/core-typings/src/uikit/index.ts @@ -0,0 +1,17 @@ +export * from '@rocket.chat/ui-kit'; +export type { + UserInteraction, + MessageBlockActionUserInteraction, + ViewBlockActionUserInteraction, + ViewClosedUserInteraction, + ViewSubmitUserInteraction, + MessageBoxActionButtonUserInteraction, + UserDropdownActionButtonUserInteraction, + MesssageActionButtonUserInteraction, + RoomActionButtonUserInteraction, +} from './UserInteraction'; +export type { View } from './View'; +export type { BannerView } from './BannerView'; +export type { ContextualBarView } from './ContextualBarView'; +export type { ModalView } from './ModalView'; +export type { ServerInteraction } from './ServerInteraction'; diff --git a/packages/core-typings/src/utils.ts b/packages/core-typings/src/utils.ts index f3f0db9da1c2..e739257c070b 100644 --- a/packages/core-typings/src/utils.ts +++ b/packages/core-typings/src/utils.ts @@ -32,3 +32,5 @@ export type DeepWritable = T extends (...args: any) => any : { -readonly [P in keyof T]: DeepWritable; }; + +export type DistributiveOmit = T extends any ? Omit : never; diff --git a/packages/fuselage-ui-kit/src/contexts/UiKitContext.ts b/packages/fuselage-ui-kit/src/contexts/UiKitContext.ts index 2c8aa02e0fa6..9e5ca1a04e5f 100644 --- a/packages/fuselage-ui-kit/src/contexts/UiKitContext.ts +++ b/packages/fuselage-ui-kit/src/contexts/UiKitContext.ts @@ -1,10 +1,15 @@ -import type { InputElementDispatchAction } from '@rocket.chat/ui-kit'; +import type { + ActionableElement, + InputElementDispatchAction, +} from '@rocket.chat/ui-kit'; import { createContext } from 'react'; +type ActionId = ActionableElement['actionId']; + type ActionParams = { blockId: string; appId: string; - actionId: string; + actionId: ActionId; value: unknown; viewId?: string; dispatchActionConfig?: InputElementDispatchAction[]; @@ -21,7 +26,7 @@ type UiKitContextValue = { ) => Promise | void; appId: string; errors?: Record; - values: Record; + values: Record; viewId?: string; rid?: string; }; diff --git a/packages/fuselage-ui-kit/src/elements/MarkdownTextElement.tsx b/packages/fuselage-ui-kit/src/elements/MarkdownTextElement.tsx index da66e4f299fb..0ff2631d7a3a 100644 --- a/packages/fuselage-ui-kit/src/elements/MarkdownTextElement.tsx +++ b/packages/fuselage-ui-kit/src/elements/MarkdownTextElement.tsx @@ -2,15 +2,16 @@ import { Markup } from '@rocket.chat/gazzodown'; import { parse } from '@rocket.chat/message-parser'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { TextObject } from '@rocket.chat/ui-kit'; +import { useContext } from 'react'; -import { useUiKitContext } from '../hooks/useUiKitContext'; +import { UiKitContext } from '../contexts/UiKitContext'; const MarkdownTextElement = ({ textObject }: { textObject: TextObject }) => { const t = useTranslation() as ( key: string, args: { [key: string]: string | number } ) => string; - const { appId } = useUiKitContext(); + const { appId } = useContext(UiKitContext); const { i18n } = textObject; diff --git a/packages/fuselage-ui-kit/src/elements/PlainTextElement.tsx b/packages/fuselage-ui-kit/src/elements/PlainTextElement.tsx index bdb59e523dee..4e692caa0993 100644 --- a/packages/fuselage-ui-kit/src/elements/PlainTextElement.tsx +++ b/packages/fuselage-ui-kit/src/elements/PlainTextElement.tsx @@ -1,14 +1,15 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { TextObject } from '@rocket.chat/ui-kit'; +import { useContext } from 'react'; -import { useUiKitContext } from '../hooks/useUiKitContext'; +import { UiKitContext } from '../contexts/UiKitContext'; const PlainTextElement = ({ textObject }: { textObject: TextObject }) => { const t = useTranslation() as ( key: string, args: { [key: string]: string | number } ) => string; - const { appId } = useUiKitContext(); + const { appId } = useContext(UiKitContext); const { i18n } = textObject; diff --git a/packages/fuselage-ui-kit/src/extractInitialStateFromLayout.ts b/packages/fuselage-ui-kit/src/extractInitialStateFromLayout.ts new file mode 100644 index 000000000000..10b6790d976a --- /dev/null +++ b/packages/fuselage-ui-kit/src/extractInitialStateFromLayout.ts @@ -0,0 +1,90 @@ +import type * as UiKit from '@rocket.chat/ui-kit'; + +type Value = { value: unknown; blockId?: string }; + +type LayoutBlockWithElement = Extract< + UiKit.LayoutBlock, + { element: UiKit.BlockElement | UiKit.TextObject } +>; +type LayoutBlockWithElements = Extract< + UiKit.LayoutBlock, + { elements: readonly (UiKit.BlockElement | UiKit.TextObject)[] } +>; + +const hasElement = ( + block: UiKit.LayoutBlock +): block is LayoutBlockWithElement => 'element' in block; + +const hasElements = ( + block: UiKit.LayoutBlock +): block is LayoutBlockWithElements => + 'elements' in block && Array.isArray(block.elements); + +const isActionableElement = ( + element: UiKit.BlockElement | UiKit.TextObject +): element is UiKit.ActionableElement => + 'actionId' in element && typeof element.actionId === 'string'; + +const hasInitialValue = ( + element: UiKit.ActionableElement +): element is UiKit.ActionableElement & { initialValue: number | string } => + 'initialValue' in element; + +const hasInitialTime = ( + element: UiKit.ActionableElement +): element is UiKit.ActionableElement & { initialTime: string } => + 'initialTime' in element; + +const hasInitialDate = ( + element: UiKit.ActionableElement +): element is UiKit.ActionableElement & { initialDate: string } => + 'initialDate' in element; + +const hasInitialOption = ( + element: UiKit.ActionableElement +): element is UiKit.ActionableElement & { initialOption: UiKit.Option } => + 'initialOption' in element; + +const hasInitialOptions = ( + element: UiKit.ActionableElement +): element is UiKit.ActionableElement & { initialOptions: UiKit.Option[] } => + 'initialOptions' in element; + +const getInitialValue = (element: UiKit.ActionableElement) => + (hasInitialValue(element) && element.initialValue) || + (hasInitialTime(element) && element.initialTime) || + (hasInitialDate(element) && element.initialDate) || + (hasInitialOption(element) && element.initialOption.value) || + (hasInitialOptions(element) && + element.initialOptions.map((option) => option.value)) || + undefined; + +const reduceInitialValuesFromLayoutBlock = ( + state: { [actionId: string]: Value }, + block: UiKit.LayoutBlock +) => { + if (hasElement(block)) { + if (isActionableElement(block.element)) { + state[block.element.actionId] = { + value: getInitialValue(block.element), + blockId: block.blockId, + }; + } + } + + if (hasElements(block)) { + for (const element of block.elements) { + if (isActionableElement(element)) { + state[element.actionId] = { + value: getInitialValue(element), + blockId: block.blockId, + }; + } + } + } + + return state; +}; + +export const extractInitialStateFromLayout = (blocks: UiKit.LayoutBlock[]) => + blocks.reduce(reduceInitialValuesFromLayoutBlock, {}); diff --git a/packages/fuselage-ui-kit/src/hooks/useUiKitContext.ts b/packages/fuselage-ui-kit/src/hooks/useUiKitContext.ts deleted file mode 100644 index 1924b96d507e..000000000000 --- a/packages/fuselage-ui-kit/src/hooks/useUiKitContext.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { useContext } from 'react'; - -import { UiKitContext } from '../contexts/UiKitContext'; - -export const useUiKitContext = () => useContext(UiKitContext); diff --git a/packages/fuselage-ui-kit/src/hooks/useUiKitState.ts b/packages/fuselage-ui-kit/src/hooks/useUiKitState.ts index 5cbae5db2b5d..56fc553b1996 100644 --- a/packages/fuselage-ui-kit/src/hooks/useUiKitState.ts +++ b/packages/fuselage-ui-kit/src/hooks/useUiKitState.ts @@ -3,16 +3,6 @@ import * as UiKit from '@rocket.chat/ui-kit'; import { useContext, useMemo, useState } from 'react'; import { UiKitContext } from '../contexts/UiKitContext'; -import { useUiKitStateValue } from './useUiKitStateValue'; - -type UiKitState< - TElement extends UiKit.ActionableElement = UiKit.ActionableElement -> = { - loading: boolean; - setLoading: (loading: boolean) => void; - error?: string; - value: UiKit.ActionOf; -}; const hasInitialValue = ( element: TElement @@ -37,10 +27,48 @@ const hasInitialOptions = ( ): element is TElement & { initialOptions: UiKit.Option[] } => 'initialOptions' in element; -export const useUiKitState: ( +const getInitialValue = ( + element: TElement +) => + (hasInitialValue(element) && element.initialValue) || + (hasInitialTime(element) && element.initialTime) || + (hasInitialDate(element) && element.initialDate) || + (hasInitialOption(element) && element.initialOption.value) || + (hasInitialOptions(element) && + element.initialOptions.map((option) => option.value)) || + undefined; + +const getElementValueFromState = ( + actionId: string, + values: Record< + string, + | { + value: unknown; + } + | undefined + >, + initialValue: string | number | string[] | undefined +) => { + return ( + (values && + (values[actionId]?.value as string | number | string[] | undefined)) ?? + initialValue + ); +}; + +type UiKitState< + TElement extends UiKit.ActionableElement = UiKit.ActionableElement +> = { + loading: boolean; + setLoading: (loading: boolean) => void; + error?: string; + value: UiKit.ActionOf; +}; + +export const useUiKitState = ( element: TElement, context: UiKit.BlockContext -) => [ +): [ state: UiKitState, action: ( pseudoEvent?: @@ -48,8 +76,8 @@ export const useUiKitState: ( | { target: EventTarget } | { target: { value: UiKit.ActionOf } } ) => void -] = (rest, context) => { - const { blockId, actionId, appId, dispatchActionConfig } = rest; +] => { + const { blockId, actionId, appId, dispatchActionConfig } = element; const { action, appId: appIdFromContext, @@ -57,16 +85,13 @@ export const useUiKitState: ( state, } = useContext(UiKitContext); - const initialValue = - (hasInitialValue(rest) && rest.initialValue) || - (hasInitialTime(rest) && rest.initialTime) || - (hasInitialDate(rest) && rest.initialDate) || - (hasInitialOption(rest) && rest.initialOption.value) || - (hasInitialOptions(rest) && - rest.initialOptions.map((option) => option.value)) || - undefined; + const initialValue = getInitialValue(element); + + const { values, errors } = useContext(UiKitContext); + + const _value = getElementValueFromState(actionId, values, initialValue); + const error = errors?.[actionId]; - const { value: _value, error } = useUiKitStateValue(actionId, initialValue); const [value, setValue] = useSafely(useState(_value)); const [loading, setLoading] = useSafely(useState(false)); @@ -147,9 +172,9 @@ export const useUiKitState: ( ); if ( - rest.type === 'plain_text_input' && - Array.isArray(rest?.dispatchActionConfig) && - rest.dispatchActionConfig.includes('on_character_entered') + element.type === 'plain_text_input' && + Array.isArray(element?.dispatchActionConfig) && + element.dispatchActionConfig.includes('on_character_entered') ) { return [result, noLoadStateActionFunction]; } @@ -159,8 +184,8 @@ export const useUiKitState: ( [UiKit.BlockContext.SECTION, UiKit.BlockContext.ACTION].includes( context )) || - (Array.isArray(rest?.dispatchActionConfig) && - rest.dispatchActionConfig.includes('on_item_selected')) + (Array.isArray(element?.dispatchActionConfig) && + element.dispatchActionConfig.includes('on_item_selected')) ) { return [result, actionFunction]; } diff --git a/packages/fuselage-ui-kit/src/hooks/useUiKitStateValue.ts b/packages/fuselage-ui-kit/src/hooks/useUiKitStateValue.ts deleted file mode 100644 index 8d7e81aa69c5..000000000000 --- a/packages/fuselage-ui-kit/src/hooks/useUiKitStateValue.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useUiKitContext } from './useUiKitContext'; - -export const useUiKitStateValue = < - T extends string | string[] | number | undefined ->( - actionId: string, - initialValue: T -): { - value: T; - error: string | undefined; -} => { - const { values, errors } = useUiKitContext(); - - return { - value: (values && (values[actionId]?.value as T)) ?? initialValue, - error: errors?.[actionId], - }; -}; diff --git a/packages/fuselage-ui-kit/src/index.ts b/packages/fuselage-ui-kit/src/index.ts index 95a713de071a..9db1f2097835 100644 --- a/packages/fuselage-ui-kit/src/index.ts +++ b/packages/fuselage-ui-kit/src/index.ts @@ -2,3 +2,4 @@ export * from './hooks/useUiKitState'; export * from './contexts/UiKitContext'; export * from './surfaces'; export { UiKitComponent } from './utils/UiKitComponent'; +export { extractInitialStateFromLayout } from './extractInitialStateFromLayout'; diff --git a/packages/mock-providers/src/MockedAppRootBuilder.tsx b/packages/mock-providers/src/MockedAppRootBuilder.tsx index 9e114be87d15..ae3eeea4cf1d 100644 --- a/packages/mock-providers/src/MockedAppRootBuilder.tsx +++ b/packages/mock-providers/src/MockedAppRootBuilder.tsx @@ -430,16 +430,13 @@ export class MockedAppRootBuilder { */} Promise.reject(new Error('not implemented')), generateTriggerId: () => '', - getUserInteractionPayloadByViewId: () => undefined, - handlePayloadUserInteraction: () => undefined, + emitInteraction: () => Promise.reject(new Error('not implemented')), + getInteractionPayloadByViewId: () => undefined, + handleServerInteraction: () => undefined, off: () => undefined, on: () => undefined, - triggerActionButtonAction: () => Promise.reject(new Error('not implemented')), - triggerBlockAction: () => Promise.reject(new Error('not implemented')), - triggerCancel: () => Promise.reject(new Error('not implemented')), - triggerSubmitView: () => Promise.reject(new Error('not implemented')), + disposeView: () => undefined, }} > {/* diff --git a/packages/rest-typings/src/apps/index.ts b/packages/rest-typings/src/apps/index.ts index 06ce6d98169e..31427afb3fee 100644 --- a/packages/rest-typings/src/apps/index.ts +++ b/packages/rest-typings/src/apps/index.ts @@ -12,6 +12,7 @@ import type { AppRequestFilter, AppRequestsStats, PaginatedAppRequests, + UiKit, } from '@rocket.chat/core-typings'; export type AppsEndpoints = { @@ -258,15 +259,6 @@ export type AppsEndpoints = { }; '/apps/ui.interaction/:id': { - POST: (params: { - type: string; - actionId: string; - rid: string; - mid: string; - viewId: string; - container: string; - triggerId: string; - payload: any; - }) => any; + POST: (params: UiKit.UserInteraction) => any; }; }; diff --git a/packages/ui-contexts/src/ActionManagerContext.ts b/packages/ui-contexts/src/ActionManagerContext.ts index d4dcdb61bfb9..76ca45cb6080 100644 --- a/packages/ui-contexts/src/ActionManagerContext.ts +++ b/packages/ui-contexts/src/ActionManagerContext.ts @@ -1,45 +1,20 @@ +import type { DistributiveOmit, UiKit } from '@rocket.chat/core-typings'; import { createContext } from 'react'; -type ActionManagerContextValue = { - on: (...args: any[]) => void; - off: (...args: any[]) => void; - generateTriggerId: (appId: any) => string; - handlePayloadUserInteraction: ( - type: any, - { - triggerId, - ...data - }: { - [x: string]: any; - triggerId: any; - }, - ) => any; - triggerAction: ({ - type, - actionId, - appId, - rid, - mid, - viewId, - container, - tmid, - ...rest - }: { - [x: string]: any; - type: any; - actionId: any; - appId: any; - rid: any; - mid: any; - viewId: any; - container: any; - tmid: any; - }) => Promise; - triggerBlockAction: (options: any) => Promise; - triggerActionButtonAction: (options: any) => Promise; - triggerSubmitView: ({ viewId, ...options }: { [x: string]: any; viewId: any }) => Promise; - triggerCancel: ({ view, ...options }: { [x: string]: any; view: any }) => Promise; - getUserInteractionPayloadByViewId: (viewId: any) => any; +type ActionManager = { + on(viewId: string, listener: (data: any) => void): void; + on(eventName: 'busy', listener: ({ busy }: { busy: boolean }) => void): void; + off(viewId: string, listener: (data: any) => any): void; + off(eventName: 'busy', listener: ({ busy }: { busy: boolean }) => void): void; + generateTriggerId(appId: string | undefined): string; + emitInteraction(appId: string, userInteraction: DistributiveOmit): Promise; + handleServerInteraction(interaction: UiKit.ServerInteraction): UiKit.ServerInteraction['type'] | undefined; + getInteractionPayloadByViewId(viewId: UiKit.ContextualBarView['id']): + | { + view: UiKit.ContextualBarView; + } + | undefined; + disposeView(viewId: UiKit.ModalView['id'] | UiKit.BannerView['viewId'] | UiKit.ContextualBarView['id']): void; }; -export const ActionManagerContext = createContext(undefined); +export const ActionManagerContext = createContext(undefined); diff --git a/yarn.lock b/yarn.lock index ce6dc859fc3d..4b4fd7f27bc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34263,7 +34263,7 @@ __metadata: "@types/chart.js": ^2.9.37 "@types/js-yaml": ^4.0.5 husky: ^7.0.4 - turbo: ~1.10.14 + turbo: ~1.10.15 languageName: unknown linkType: soft @@ -37678,58 +37678,58 @@ __metadata: languageName: node linkType: hard -"turbo-darwin-64@npm:1.10.14": - version: 1.10.14 - resolution: "turbo-darwin-64@npm:1.10.14" +"turbo-darwin-64@npm:1.10.15": + version: 1.10.15 + resolution: "turbo-darwin-64@npm:1.10.15" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"turbo-darwin-arm64@npm:1.10.14": - version: 1.10.14 - resolution: "turbo-darwin-arm64@npm:1.10.14" +"turbo-darwin-arm64@npm:1.10.15": + version: 1.10.15 + resolution: "turbo-darwin-arm64@npm:1.10.15" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"turbo-linux-64@npm:1.10.14": - version: 1.10.14 - resolution: "turbo-linux-64@npm:1.10.14" +"turbo-linux-64@npm:1.10.15": + version: 1.10.15 + resolution: "turbo-linux-64@npm:1.10.15" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"turbo-linux-arm64@npm:1.10.14": - version: 1.10.14 - resolution: "turbo-linux-arm64@npm:1.10.14" +"turbo-linux-arm64@npm:1.10.15": + version: 1.10.15 + resolution: "turbo-linux-arm64@npm:1.10.15" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"turbo-windows-64@npm:1.10.14": - version: 1.10.14 - resolution: "turbo-windows-64@npm:1.10.14" +"turbo-windows-64@npm:1.10.15": + version: 1.10.15 + resolution: "turbo-windows-64@npm:1.10.15" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"turbo-windows-arm64@npm:1.10.14": - version: 1.10.14 - resolution: "turbo-windows-arm64@npm:1.10.14" +"turbo-windows-arm64@npm:1.10.15": + version: 1.10.15 + resolution: "turbo-windows-arm64@npm:1.10.15" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"turbo@npm:~1.10.14": - version: 1.10.14 - resolution: "turbo@npm:1.10.14" +"turbo@npm:~1.10.15": + version: 1.10.15 + resolution: "turbo@npm:1.10.15" dependencies: - turbo-darwin-64: 1.10.14 - turbo-darwin-arm64: 1.10.14 - turbo-linux-64: 1.10.14 - turbo-linux-arm64: 1.10.14 - turbo-windows-64: 1.10.14 - turbo-windows-arm64: 1.10.14 + turbo-darwin-64: 1.10.15 + turbo-darwin-arm64: 1.10.15 + turbo-linux-64: 1.10.15 + turbo-linux-arm64: 1.10.15 + turbo-windows-64: 1.10.15 + turbo-windows-arm64: 1.10.15 dependenciesMeta: turbo-darwin-64: optional: true @@ -37745,7 +37745,7 @@ __metadata: optional: true bin: turbo: bin/turbo - checksum: 219d245bb5cc32a9f76b136b81e86e179228d93a44cab4df3e3d487a55dd2688b5b85f4d585b66568ac53166145352399dd2d7ed0cd47f1aae63d08beb814ebb + checksum: b494c8bf79355874919e76ee0e4a0a53616e0ae5c7126eb1add50e67d4cd1e445ed9aecf99cb6d81c592b7a43ba91cd7dbf30df70410a44cecedba8b5126095d languageName: node linkType: hard From ab0c287a62e91a42ebd4b0da8b0fec76d5f8950e Mon Sep 17 00:00:00 2001 From: Akash Bag <110753356+akash0708@users.noreply.github.com> Date: Thu, 19 Oct 2023 19:15:17 +0530 Subject: [PATCH 28/38] docs: Fixed typo in FEATURES.md (#30683) --- FEATURES.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/FEATURES.md b/FEATURES.md index e4b11c141184..5601cc0c7ccc 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -1,10 +1,10 @@ # Features - Self Host - - Docker - - Multiple Deployment Options (Heroku, Digital Ocean, Sandstorm, etc.) + - Docker + - Multiple Deployment Options (Heroku, Digital Ocean, Sandstorm, etc.) - Authentication Options - - OAuth + - OAuth - SAML - LDAP - CAS (1.0, 2.0 + attribute sync) @@ -19,7 +19,7 @@ - Rich Media - Audio Calls - Video Conferencing - - Screensharing + - Screen Sharing - Notifications - Desktop and Mobile - Use your own gateway From f7b07a0fc58f4c3b46fe1dea773f6a433fed720e Mon Sep 17 00:00:00 2001 From: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com> Date: Thu, 19 Oct 2023 20:47:38 +0530 Subject: [PATCH 29/38] feat: Allow CE users to customise Business hour timezone (#30565) --- .changeset/tidy-cows-destroy.md | 5 ++++ .../views/app/business-hours/BusinessHours.ts | 4 --- .../business-hours/IBusinessHourBehavior.ts | 1 - .../client/views/app/business-hours/Single.ts | 4 --- .../BusinessHoursFormContainer.js | 15 +++++----- .../businessHours}/BusinessHoursTimeZone.js | 4 +-- .../BusinessHoursTimeZone.stories.tsx | 2 +- .../client/SingleBusinessHour.ts | 7 ----- .../app/livechat-enterprise/client/startup.ts | 9 +++--- .../client/views/business-hours/Multiple.ts | 4 --- .../server/business-hour/Helper.ts | 29 +------------------ .../app/livechat-enterprise/server/startup.ts | 3 -- .../omnichannel/additionalForms/register.ts | 3 -- 13 files changed, 22 insertions(+), 68 deletions(-) create mode 100644 .changeset/tidy-cows-destroy.md rename apps/meteor/{ee/client/omnichannel/additionalForms => client/views/omnichannel/businessHours}/BusinessHoursTimeZone.js (86%) rename apps/meteor/{ee/client/omnichannel/additionalForms => client/views/omnichannel/businessHours}/BusinessHoursTimeZone.stories.tsx (91%) delete mode 100644 apps/meteor/ee/app/livechat-enterprise/client/SingleBusinessHour.ts diff --git a/.changeset/tidy-cows-destroy.md b/.changeset/tidy-cows-destroy.md new file mode 100644 index 000000000000..0b222f8157a9 --- /dev/null +++ b/.changeset/tidy-cows-destroy.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +feat: Community users will now be able to customize their Business hour timezone diff --git a/apps/meteor/app/livechat/client/views/app/business-hours/BusinessHours.ts b/apps/meteor/app/livechat/client/views/app/business-hours/BusinessHours.ts index 0935ac7554e4..3c723cc46257 100644 --- a/apps/meteor/app/livechat/client/views/app/business-hours/BusinessHours.ts +++ b/apps/meteor/app/livechat/client/views/app/business-hours/BusinessHours.ts @@ -29,10 +29,6 @@ class BusinessHoursManager { showBackButton(): boolean { return this.behavior.showBackButton(); } - - showTimezoneTemplate(): boolean { - return this.behavior.showTimezoneTemplate(); - } } export const businessHourManager = new BusinessHoursManager(new SingleBusinessHourBehavior()); diff --git a/apps/meteor/app/livechat/client/views/app/business-hours/IBusinessHourBehavior.ts b/apps/meteor/app/livechat/client/views/app/business-hours/IBusinessHourBehavior.ts index 10a51fe25e25..1ba6e4a56907 100644 --- a/apps/meteor/app/livechat/client/views/app/business-hours/IBusinessHourBehavior.ts +++ b/apps/meteor/app/livechat/client/views/app/business-hours/IBusinessHourBehavior.ts @@ -4,5 +4,4 @@ export interface IBusinessHourBehavior { getView(): string; showCustomTemplate(businessHourData: ILivechatBusinessHour): boolean; showBackButton(): boolean; - showTimezoneTemplate(): boolean; } diff --git a/apps/meteor/app/livechat/client/views/app/business-hours/Single.ts b/apps/meteor/app/livechat/client/views/app/business-hours/Single.ts index 1ce09a63d79a..9b343264ca54 100644 --- a/apps/meteor/app/livechat/client/views/app/business-hours/Single.ts +++ b/apps/meteor/app/livechat/client/views/app/business-hours/Single.ts @@ -12,8 +12,4 @@ export class SingleBusinessHourBehavior implements IBusinessHourBehavior { showBackButton(): boolean { return false; } - - showTimezoneTemplate(): boolean { - return false; - } } diff --git a/apps/meteor/client/views/omnichannel/businessHours/BusinessHoursFormContainer.js b/apps/meteor/client/views/omnichannel/businessHours/BusinessHoursFormContainer.js index c274ebee272d..9acf9d2fd167 100644 --- a/apps/meteor/client/views/omnichannel/businessHours/BusinessHoursFormContainer.js +++ b/apps/meteor/client/views/omnichannel/businessHours/BusinessHoursFormContainer.js @@ -7,6 +7,7 @@ import { useForm } from '../../../hooks/useForm'; import { useReactiveValue } from '../../../hooks/useReactiveValue'; import { useFormsSubscription } from '../additionalForms'; import BusinessHourForm from './BusinessHoursForm'; +import BusinessHoursTimeZone from './BusinessHoursTimeZone'; const useChangeHandler = (name, ref) => useMutableCallback((val) => { @@ -29,12 +30,10 @@ const BusinessHoursFormContainer = ({ data, saveRef, onChange = () => {} }) => { const [hasChangesMultiple, setHasChangesMultiple] = useState(false); const [hasChangesTimeZone, setHasChangesTimeZone] = useState(false); - const { useBusinessHoursTimeZone = cleanFunc, useBusinessHoursMultiple = cleanFunc } = forms; + const { useBusinessHoursMultiple = cleanFunc } = forms; - const TimezoneForm = useBusinessHoursTimeZone(); const MultipleBHForm = useBusinessHoursMultiple(); - const showTimezone = useReactiveValue(useMutableCallback(() => businessHourManager.showTimezoneTemplate())); const showMultipleBHForm = useReactiveValue(useMutableCallback(() => businessHourManager.showCustomTemplate(data))); const onChangeTimezone = useChangeHandler('timezone', saveRef); @@ -45,7 +44,7 @@ const BusinessHoursFormContainer = ({ data, saveRef, onChange = () => {} }) => { saveRef.current.form = values; useEffect(() => { - onChange(hasUnsavedChanges || (showMultipleBHForm && hasChangesMultiple) || (showTimezone && hasChangesTimeZone)); + onChange(hasUnsavedChanges || (showMultipleBHForm && hasChangesMultiple) || hasChangesTimeZone); }); return ( @@ -54,9 +53,11 @@ const BusinessHoursFormContainer = ({ data, saveRef, onChange = () => {} }) => { {showMultipleBHForm && MultipleBHForm && ( )} - {showTimezone && TimezoneForm && ( - - )} +
diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/BusinessHoursTimeZone.js b/apps/meteor/client/views/omnichannel/businessHours/BusinessHoursTimeZone.js similarity index 86% rename from apps/meteor/ee/client/omnichannel/additionalForms/BusinessHoursTimeZone.js rename to apps/meteor/client/views/omnichannel/businessHours/BusinessHoursTimeZone.js index c305dc084e68..8826fc621455 100644 --- a/apps/meteor/ee/client/omnichannel/additionalForms/BusinessHoursTimeZone.js +++ b/apps/meteor/client/views/omnichannel/businessHours/BusinessHoursTimeZone.js @@ -2,8 +2,8 @@ import { SelectFiltered, Field } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useMemo } from 'react'; -import { useForm } from '../../../../client/hooks/useForm'; -import { useTimezoneNameList } from '../../../../client/hooks/useTimezoneNameList'; +import { useForm } from '../../../hooks/useForm'; +import { useTimezoneNameList } from '../../../hooks/useTimezoneNameList'; const getInitialData = (data = {}) => ({ name: data ?? '', diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/BusinessHoursTimeZone.stories.tsx b/apps/meteor/client/views/omnichannel/businessHours/BusinessHoursTimeZone.stories.tsx similarity index 91% rename from apps/meteor/ee/client/omnichannel/additionalForms/BusinessHoursTimeZone.stories.tsx rename to apps/meteor/client/views/omnichannel/businessHours/BusinessHoursTimeZone.stories.tsx index 35c133caf72a..af00db33322e 100644 --- a/apps/meteor/ee/client/omnichannel/additionalForms/BusinessHoursTimeZone.stories.tsx +++ b/apps/meteor/client/views/omnichannel/businessHours/BusinessHoursTimeZone.stories.tsx @@ -5,7 +5,7 @@ import React from 'react'; import BusinessHoursTimeZone from './BusinessHoursTimeZone'; export default { - title: 'Enterprise/Omnichannel/BusinessHoursTimeZone', + title: 'Omnichannel/BusinessHoursTimeZone', component: BusinessHoursTimeZone, decorators: [ (fn) => ( diff --git a/apps/meteor/ee/app/livechat-enterprise/client/SingleBusinessHour.ts b/apps/meteor/ee/app/livechat-enterprise/client/SingleBusinessHour.ts deleted file mode 100644 index e1f8b852fbc6..000000000000 --- a/apps/meteor/ee/app/livechat-enterprise/client/SingleBusinessHour.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { SingleBusinessHourBehavior } from '../../../../app/livechat/client/views/app/business-hours/Single'; - -export class EESingleBusinessHourBehaviour extends SingleBusinessHourBehavior { - showTimezoneTemplate(): boolean { - return true; - } -} diff --git a/apps/meteor/ee/app/livechat-enterprise/client/startup.ts b/apps/meteor/ee/app/livechat-enterprise/client/startup.ts index 11027a62439b..3c3ec1c02139 100644 --- a/apps/meteor/ee/app/livechat-enterprise/client/startup.ts +++ b/apps/meteor/ee/app/livechat-enterprise/client/startup.ts @@ -2,20 +2,21 @@ import { Meteor } from 'meteor/meteor'; import { businessHourManager } from '../../../../app/livechat/client/views/app/business-hours/BusinessHours'; import type { IBusinessHourBehavior } from '../../../../app/livechat/client/views/app/business-hours/IBusinessHourBehavior'; +import { SingleBusinessHourBehavior } from '../../../../app/livechat/client/views/app/business-hours/Single'; import { settings } from '../../../../app/settings/client'; import { hasLicense } from '../../license/client'; -import { EESingleBusinessHourBehaviour } from './SingleBusinessHour'; import { MultipleBusinessHoursBehavior } from './views/business-hours/Multiple'; const businessHours: Record = { multiple: new MultipleBusinessHoursBehavior(), - single: new EESingleBusinessHourBehaviour(), + single: new SingleBusinessHourBehavior(), }; Meteor.startup(() => { - settings.onload('Livechat_business_hour_type', async (_, value) => { + Tracker.autorun(async () => { + const bhType = settings.get('Livechat_business_hour_type'); if (await hasLicense('livechat-enterprise')) { - businessHourManager.registerBusinessHourBehavior(businessHours[(value as string).toLowerCase()]); + businessHourManager.registerBusinessHourBehavior(businessHours[bhType.toLowerCase()]); } }); }); diff --git a/apps/meteor/ee/app/livechat-enterprise/client/views/business-hours/Multiple.ts b/apps/meteor/ee/app/livechat-enterprise/client/views/business-hours/Multiple.ts index cbe3fe9088fe..a57344da73dc 100644 --- a/apps/meteor/ee/app/livechat-enterprise/client/views/business-hours/Multiple.ts +++ b/apps/meteor/ee/app/livechat-enterprise/client/views/business-hours/Multiple.ts @@ -12,10 +12,6 @@ export class MultipleBusinessHoursBehavior implements IBusinessHourBehavior { return !businessHourData._id || businessHourData.type !== LivechatBusinessHourTypes.DEFAULT; } - showTimezoneTemplate(): boolean { - return true; - } - showBackButton(): boolean { return true; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts index a441e122ef99..a91bb87f28bb 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts @@ -1,8 +1,6 @@ import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; -import { License } from '@rocket.chat/license'; -import { LivechatBusinessHours, LivechatDepartment, LivechatDepartmentAgents, Users } from '@rocket.chat/models'; -import moment from 'moment-timezone'; +import { LivechatDepartment, LivechatDepartmentAgents, Users } from '@rocket.chat/models'; import { businessHourLogger } from '../../../../../app/livechat/server/lib/logger'; @@ -103,28 +101,3 @@ export const removeBusinessHourByAgentIds = async (agentIds: string[], businessH await Users.removeBusinessHourByAgentIds(agentIds, businessHourId); await Users.updateLivechatStatusBasedOnBusinessHours(); }; - -export const resetDefaultBusinessHourIfNeeded = async (): Promise => { - if (License.hasValidLicense()) { - return; - } - - const defaultBusinessHour = await LivechatBusinessHours.findOneDefaultBusinessHour>({ - projection: { _id: 1 }, - }); - if (!defaultBusinessHour) { - return; - } - - await LivechatBusinessHours.updateOne( - { _id: defaultBusinessHour._id }, - { - $set: { - timezone: { - name: moment.tz.guess(), - utc: String(moment().utcOffset() / 60), - }, - }, - }, - ); -}; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/startup.ts b/apps/meteor/ee/app/livechat-enterprise/server/startup.ts index 29cf5c308d71..1b277d6fba52 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/startup.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/startup.ts @@ -3,7 +3,6 @@ import { Meteor } from 'meteor/meteor'; import { businessHourManager } from '../../../../app/livechat/server/business-hour'; import { SingleBusinessHourBehavior } from '../../../../app/livechat/server/business-hour/Single'; import { settings } from '../../../../app/settings/server'; -import { resetDefaultBusinessHourIfNeeded } from './business-hour/Helper'; import { MultipleBusinessHoursBehavior } from './business-hour/Multiple'; import { updatePredictedVisitorAbandonment, updateQueueInactivityTimeout } from './lib/Helper'; import { VisitorInactivityMonitor } from './lib/VisitorInactivityMonitor'; @@ -43,6 +42,4 @@ Meteor.startup(async () => { logger.debug(`Business hour manager started`); } }); - - await resetDefaultBusinessHourIfNeeded(); }); diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/register.ts b/apps/meteor/ee/client/omnichannel/additionalForms/register.ts index df11a435ab5c..d52a20e40734 100644 --- a/apps/meteor/ee/client/omnichannel/additionalForms/register.ts +++ b/apps/meteor/ee/client/omnichannel/additionalForms/register.ts @@ -6,7 +6,6 @@ import { registerForm } from '../../../../client/views/omnichannel/additionalFor import { hasLicense } from '../../../app/license/client'; import type CurrentChatTags from '../tags/CurrentChatTags'; import type BusinessHoursMultipleContainer from './BusinessHoursMultipleContainer'; -import type BusinessHoursTimeZone from './BusinessHoursTimeZone'; import type ContactManager from './ContactManager'; import type CustomFieldsAdditionalFormContainer from './CustomFieldsAdditionalFormContainer'; import type DepartmentBusinessHours from './DepartmentBusinessHours'; @@ -29,7 +28,6 @@ declare module '../../../../client/views/omnichannel/additionalForms' { useEeTextAreaInput?: () => LazyExoticComponent; useBusinessHoursMultiple?: () => LazyExoticComponent; useEeTextInput?: () => LazyExoticComponent; - useBusinessHoursTimeZone?: () => LazyExoticComponent; useContactManager?: () => LazyExoticComponent; useCurrentChatTags?: () => LazyExoticComponent; @@ -54,7 +52,6 @@ hasLicense('livechat-enterprise').then((enabled) => { useEeTextAreaInput: () => useMemo(() => lazy(() => import('./EeTextAreaInput')), []), useBusinessHoursMultiple: () => useMemo(() => lazy(() => import('./BusinessHoursMultipleContainer')), []), useEeTextInput: () => useMemo(() => lazy(() => import('./EeTextInput')), []), - useBusinessHoursTimeZone: () => useMemo(() => lazy(() => import('./BusinessHoursTimeZone')), []), useContactManager: () => useMemo(() => lazy(() => import('./ContactManager')), []), useCurrentChatTags: () => useMemo(() => lazy(() => import('../tags/CurrentChatTags')), []), useDepartmentBusinessHours: () => useMemo(() => lazy(() => import('./DepartmentBusinessHours')), []), From 704ed0fc7b77b1086d2819072a9bdaad714a28b0 Mon Sep 17 00:00:00 2001 From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Thu, 19 Oct 2023 22:44:31 +0530 Subject: [PATCH 30/38] fix: i18n translations using sprintf post processor (#30685) --- .changeset/perfect-onions-develop.md | 5 +++++ apps/meteor/app/utils/lib/i18n.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/perfect-onions-develop.md diff --git a/.changeset/perfect-onions-develop.md b/.changeset/perfect-onions-develop.md new file mode 100644 index 000000000000..3ca5c3e00bb7 --- /dev/null +++ b/.changeset/perfect-onions-develop.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fix i18n translations using sprintf post processor diff --git a/apps/meteor/app/utils/lib/i18n.ts b/apps/meteor/app/utils/lib/i18n.ts index 13d5c667709d..7fa491d965e8 100644 --- a/apps/meteor/app/utils/lib/i18n.ts +++ b/apps/meteor/app/utils/lib/i18n.ts @@ -7,7 +7,7 @@ export const i18n = i18next.use(sprintf); export const addSprinfToI18n = function (t: (typeof i18n)['t']) { return function (key: string, ...replaces: any): string { - if (replaces[0] === undefined || isObject(replaces[0])) { + if (replaces[0] === undefined || (isObject(replaces[0]) && !Array.isArray(replaces[0]))) { return t(key, ...replaces); } From 5b3ff91d1bb09f78ff19b92a4997d09b30d2597f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Thu, 19 Oct 2023 16:31:40 -0300 Subject: [PATCH 31/38] feat: move a11y features to CE (#30676) --- .../providers/UserProvider/UserProvider.tsx | 9 -- .../accessibility/AccessibilityPage.tsx | 80 ++++-------------- .../accessibility/HighContrastUpsellModal.tsx | 41 --------- .../MentionsWithSymbolUpsellModal.tsx | 40 --------- .../views/account/accessibility/themeItems.ts | 10 ++- .../rocketchat-i18n/i18n/en.i18n.json | 7 -- .../images/high-contrast-upsell-modal.png | Bin 13392 -> 0 bytes .../public/images/mentions-upsell-modal.png | Bin 9723 -> 0 bytes 8 files changed, 24 insertions(+), 163 deletions(-) delete mode 100644 apps/meteor/client/views/account/accessibility/HighContrastUpsellModal.tsx delete mode 100644 apps/meteor/client/views/account/accessibility/MentionsWithSymbolUpsellModal.tsx delete mode 100644 apps/meteor/public/images/high-contrast-upsell-modal.png delete mode 100644 apps/meteor/public/images/mentions-upsell-modal.png diff --git a/apps/meteor/client/providers/UserProvider/UserProvider.tsx b/apps/meteor/client/providers/UserProvider/UserProvider.tsx index 432a197671f3..09f631ffa6a6 100644 --- a/apps/meteor/client/providers/UserProvider/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider/UserProvider.tsx @@ -10,7 +10,6 @@ import { Subscriptions, ChatRoom } from '../../../app/models/client'; import { getUserPreference } from '../../../app/utils/client'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; import { afterLogoutCleanUpCallback } from '../../../lib/callbacks/afterLogoutCleanUpCallback'; -import { useIsEnterprise } from '../../hooks/useIsEnterprise'; import { useReactiveValue } from '../../hooks/useReactiveValue'; import { createReactiveSubscriptionFactory } from '../../lib/createReactiveSubscriptionFactory'; import { useCreateFontStyleElement } from '../../views/account/accessibility/hooks/useCreateFontStyleElement'; @@ -180,14 +179,6 @@ const UserProvider = ({ children }: UserProviderProps): ReactElement => { } }, [preferedLanguage, setPreferedLanguage, setUserLanguage, user?.language, userLanguage, userId, setUserPreferences]); - const { data: license } = useIsEnterprise({ enabled: !!userId }); - - useEffect(() => { - if (!license?.isEnterprise && user?.settings?.preferences?.themeAppearence === 'high-contrast') { - setUserPreferences({ data: { themeAppearence: 'light' } }); - } - }, [license?.isEnterprise, setUserPreferences, user?.settings?.preferences?.themeAppearence]); - return ; }; diff --git a/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx b/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx index 657548d5a1b9..c8179f08bef2 100644 --- a/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx +++ b/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx @@ -1,7 +1,6 @@ import { css } from '@rocket.chat/css-in-js'; import type { SelectOption } from '@rocket.chat/fuselage'; import { - Icon, FieldDescription, Accordion, Box, @@ -14,20 +13,16 @@ import { FieldRow, RadioButton, Select, - Tag, ToggleSwitch, } from '@rocket.chat/fuselage'; -import { useLocalStorage, useUniqueId } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useTranslation, useToastMessageDispatch, useEndpoint, useSetting } from '@rocket.chat/ui-contexts'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useTranslation, useToastMessageDispatch, useEndpoint, useSetting } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; import React, { useMemo } from 'react'; import { Controller, useForm } from 'react-hook-form'; import Page from '../../../components/Page'; -import { useIsEnterprise } from '../../../hooks/useIsEnterprise'; import { getDirtyFields } from '../../../lib/getDirtyFields'; -import HighContrastUpsellModal from './HighContrastUpsellModal'; -import MentionsWithSymbolUpsellModal from './MentionsWithSymbolUpsellModal'; import { fontSizes } from './fontSizes'; import type { AccessibilityPreferencesData } from './hooks/useAcessibilityPreferencesValues'; import { useAccessiblityPreferencesValues } from './hooks/useAcessibilityPreferencesValues'; @@ -36,14 +31,9 @@ import { themeItems as themes } from './themeItems'; const AccessibilityPage = () => { const t = useTranslation(); - const setModal = useSetModal(); const dispatchToastMessage = useToastMessageDispatch(); const preferencesValues = useAccessiblityPreferencesValues(); - const { data: license } = useIsEnterprise(); - const isEnterprise = license?.isEnterprise; - const { themeAppearence } = preferencesValues; - const [, setPrevTheme] = useLocalStorage('prevTheme', themeAppearence); const createFontStyleElement = useCreateFontStyleElement(); const displayRolesEnabled = useSetting('UI_DisplayRoles'); @@ -82,7 +72,6 @@ const AccessibilityPage = () => { onError: (error) => dispatchToastMessage({ type: 'error', message: error }), onSettled: (_data, _error, { data: { fontSize } }) => { reset(currentData); - dirtyFields.themeAppearence && setPrevTheme(themeAppearence); dirtyFields.fontSize && fontSize && createFontStyleElement(fontSize); }, }); @@ -102,45 +91,25 @@ const AccessibilityPage = () => {
- {themes.map(({ id, title, description, ...item }, index) => { - const showCommunityUpsellTriggers = 'isEEOnly' in item && item.isEEOnly && !isEnterprise; - + {themes.map(({ id, title, description }, index) => { return ( - {t.has(title) ? t(title) : title} - {showCommunityUpsellTriggers && ( - - - - {t('Enterprise')} - - - )} + {t(title)} { - if (showCommunityUpsellTriggers) { - return ( - setModal( setModal(null)} />)} - checked={false} - /> - ); - } - return onChange(id)} checked={value === id} />; - }} + render={({ field: { onChange, value, ref } }) => ( + onChange(id)} checked={value === id} /> + )} /> - {t.has(description) ? t(description) : description} + {t(description)} ); @@ -165,32 +134,15 @@ const AccessibilityPage = () => { - - {t('Mentions_with_@_symbol')} - {!isEnterprise && ( - - - - {t('Enterprise')} - - - )} - + {t('Mentions_with_@_symbol')} - {isEnterprise ? ( - ( - - )} - /> - ) : ( - setModal( setModal(null)} />)} - checked={false} - /> - )} + ( + + )} + /> void }) => { - const t = useTranslation(); - - const isAdmin = useRole('admin'); - const { handleGoFullyFeatured, handleTalkToSales } = useUpsellActions(); - - if (!isAdmin) { - return ( - - ); - } - return ( - - ); -}; -export default HighContrastUpsellModal; diff --git a/apps/meteor/client/views/account/accessibility/MentionsWithSymbolUpsellModal.tsx b/apps/meteor/client/views/account/accessibility/MentionsWithSymbolUpsellModal.tsx deleted file mode 100644 index b92ca74d0f6e..000000000000 --- a/apps/meteor/client/views/account/accessibility/MentionsWithSymbolUpsellModal.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useRole, useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; - -import GenericUpsellModal from '../../../components/GenericUpsellModal'; -import { useUpsellActions } from '../../../components/GenericUpsellModal/hooks'; - -const MentionsWithSymbolUpsellModal = ({ onClose }: { onClose: () => void }) => { - const t = useTranslation(); - - const isAdmin = useRole('admin'); - const { handleGoFullyFeatured, handleTalkToSales } = useUpsellActions(); - - if (!isAdmin) { - return ( - - ); - } - return ( - - ); -}; -export default MentionsWithSymbolUpsellModal; diff --git a/apps/meteor/client/views/account/accessibility/themeItems.ts b/apps/meteor/client/views/account/accessibility/themeItems.ts index 62bf3830d952..f16d9128503d 100644 --- a/apps/meteor/client/views/account/accessibility/themeItems.ts +++ b/apps/meteor/client/views/account/accessibility/themeItems.ts @@ -1,4 +1,11 @@ -export const themeItems = [ +import type { TranslationKey } from '@rocket.chat/ui-contexts'; + +type ThemeItem = { + id: string; + title: TranslationKey; + description: TranslationKey; +}; +export const themeItems: ThemeItem[] = [ { id: 'light', title: 'Theme_light', @@ -10,7 +17,6 @@ export const themeItems = [ description: 'Theme_dark_description', }, { - isEEOnly: true, id: 'high-contrast', title: 'Theme_high_contrast', description: 'Theme_high_contrast_description', diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 7ef10e0988b0..46805a1f0e3e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1100,7 +1100,6 @@ "Condition": "Condition", "Commit_details": "Commit Details", "Completed": "Completed", - "Compliant_use_of_color": "Compliant use of color", "Computer": "Computer", "Conference_call_apps": "Conference call apps", "Conference_call_has_ended": "_Call has ended._", @@ -1865,7 +1864,6 @@ "EmojiCustomFilesystem_Description": "Specify how emojis are stored.", "Empty_no_agent_selected": "Empty, no agent selected", "Empty_title": "Empty title", - "Empower_access_move_beyond_color": "Empower access, move beyond color", "Enable": "Enable", "Enable_Auto_Away": "Enable Auto Away", "Enable_CSP": "Enable Content-Security-Policy", @@ -3364,7 +3362,6 @@ "Mentions_only": "Mentions only", "Mentions_with_@_symbol": "Mentions with @ symbol", "Mentions_with_@_symbol_description": "Mentions notify and highlight messages for groups or specific users, facilitating targeted communication.\n\nThe screen reader functionality is optimized when the \"@\" symbol is employed in the mention feature. This ensures that users relying on screen readers can easily interpret and engage with these mentions.", - "Mentions_with_symbol_upsell_description": "Unlock the full potential of a barrier-free business with our premium accessibility feature.\n\nSay goodbye to color-related compliance challenges all while aligning with WCAG (Web Content Accessibility Guidelines) and BITV (Barrierefreie Informationstechnik-Verordnung) standards.\n\nThe use of the @ symbol makes it easier for screen readers to navigate and interact with your content, ensuring the best experience for all users.", "Merge_Channels": "Merge Channels", "message": "message", "Message": "Message", @@ -5976,10 +5973,6 @@ "Theme_high_contrast": "High contrast", "Theme_high_contrast_description": "Maximum tonal differentiation with bold colors and sharp contrasts provide enhanced accessibility.", "Highlighted_chosen_word": "Highlighted chosen word", - "High_contrast_upsell_title": "Enable high contrast theme", - "High_contrast_upsell_subtitle": "Enhance your team’s reading experience", - "High_contrast_upsell_description": "Especially designed for individuals with visual impairments or conditions such as color vision deficiency, low vision, or sensitivity to low contrast.\n\nThis theme increases contrast between text and background elements, making content more distinguishable and easier to read.", - "High_contrast_upsell_annotation": "Talk to your workspace admin about enabling the high contrast theme for everyone.", "Join_your_team": "Join your team", "Create_a_password": "Create a password", "Create_an_account": "Create an account", diff --git a/apps/meteor/public/images/high-contrast-upsell-modal.png b/apps/meteor/public/images/high-contrast-upsell-modal.png deleted file mode 100644 index b761a1b0b76c81cda36007039682c3a5493b8448..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13392 zcmc(GbySq!_bw?gfHa6A-Q6wS9nv)--5ny0NJ%#XC?H+ZA*pl;2nYj=2n^COblw;J zeB-X)y?@?y|6tAHT{GvLcb~nVv-h)~6RoA8hzoiQLPA2qRaTPIK|(?SBOxIpVLkw! z5c>|30UuayN=BYYNZ3U8f5=GLkVn8nWKSJM8KmlQicR1jG+SwPX(Xgi3E0;!(2V>B6y!z2XA3%}N?xv7rL+PRDXb1DKjyUT3V#LQ zO4#0MY@CgbkMq^@wbuunj$XstVF`P!?PBeS9dWTP|2&v@SMc)6BsYJ|Bb>Lzux)bU z{*>s$XA|e0uK1Z6B{qa7WzgNn#F2$bo5&}&{FFHw^sY+0Lb$R-UZM}shaoQb)(@Rs zNU~B8tMUj(lfwoUa0I>#Ce3f~V)gxJL<1)4-zNfv8puy@9z4K0M8=k-V5>P-h`Iw2 zep&fOP18wW$ICQFK$t4BL_JKguoy$_xkX%C)+2+4W)H3lIl&miBGiWeynI>vest4z z7*Y5zn;MG-n8!iEjJSyXrXYeBJ34(Cu4P09%_-Gnz9EpQy&V*wzxgn_`G&XT$uJ}c z-wVC?1NHsV%Eh9Jin8t{-yIxV5f-6#jnZyA;D z2TaV?E(!S{pGPhKfZ_c~^vxvRM3M`$=cRP{Cw6KG)&H1Q7Cr8Mn~H3{vw*{Ux~W4+ z(OEu>+7Xz_cK@d4&kBzXD;h0(pbh^9*GJ3JEeUyH&Dt}{^zxg>BXagkAyvs#k(=&y zJ z+%Am%@78#O9vSEMV1YT58h*d$ky7U&18krLe=eIfjklp6SgPCI4<><@fG4G=GFX0e z^CPE-Y~n#iL*?DH|5(>fXdH76|KCf|K*sI{MP5oJNrxTx?R()6KK#gF0hy$fIAatN(7ztn@x8e*e|N;q1JnNc6zB{ z@}*Vd+qU<{5rgwkz!CFm6Li)sb9wRjYSTY;SJ~vqA9iNrtiFM0ZGi3e_kd_6T{8Obu z@k%8_P<}}<~8JYO%DQMA#?<_ z|Nnv^@j9ime>`?&3BOX(1J_?l{wIs)B@mAo+VF*HHf~(>`^lBYzWpcUxf?{oiMovw z?IxvPhGbiEM)3-L7%hwBEuk8(y3{HOdDp|*q}fvtUc{wa8)}cj=s86toP`?uvtV@d zKCoOM1l2VE1xchx@=_oPEvrQ8z{60`cTjE&ufeOw*vrD>ERxj?O2J3GzEixvoe!Sr zNwOws1oh$7T;4k0K6EiL|A3PkY)}GscUcOmVUv{SowL~y#`sP}1NgWchLi!An4U&8 zF_w-zGKJeks4>dXN15?`!FRfgiFMgt1pLVvpZq?_9#NN3i-nQcMdw#_Tc$kPX&r3~ zgFuV<4JS)jm_(IB{rUN{&(Xw)n$>bP%xv}y9sTR3!7buqpY4Xy{RQ);#r&uA#tg|> z>n_#FE4@?qt6g)kh(!6)5CUFVIG#IO)JDflN~!{3U}R!M@mllFgE#nug)ZuK2O4#K z#8r_EaskPIp#IC_=&eTv{}&YGo3!RpqGdB4A4fJ zzNv5x44c}tXj$64Hr3aL3r)Y7c;_TPIF?4|yfK7_DZy=q^~Sxj6FW7Y+vKhmsLOtm zfiFZ3E|zsX3lAe+b-B9eIU1%bLg~8|k0mlN^ux&ax8_f-{LyGV<3eJ0Lr{B3WTE+K zn8c^SA3TrvpA$R z0Y5_NMtLG(ZG1THA-S&A=Pi<}-?rgI{P8SyFNKmXw55b4y?U9%6qi4B9`gjP>Nz`dWBk;kl$P zs=?`narotpEG zx1V=ytAYj0A66p7Y~tecPB2GUUoawkSYw~}4#xjY35q9}l^T`gvX9H=cC!WRe)|$B zO8b12jIZ}CnT~Ek7aUfgZO5e9=E?dxSrOjD{a6UHJ${HiT^=zc%yjF7tTltz(5hEJ zy?)J&+eg~Ns-06^Dz&uUIO{LG?UgUQ`qmxO>iSVGld|&Uexq&=y&QIKyzAYo$#ZOyNzgGQ;=irSB6kfrAxu<85Ed(8HvM$DR;9F7oS|fFa5>E}5n{Q(dmWO_rvgJg-HA!Vq=}D)&1TUco@^!u zK;q+fID{!fXu~xBJnmOD!LD0hHKn}A zD534R5<~aIY5bbVC1NLVGPY4chIo$Xq@kEYjPrPUh94i6qRNRkyld1#Ljy)Hi2rl+Qf~H z?ItU%2&}3Q$S-*wsL4)ty4g%zz}VS?l^=s-l=vcAnI1=~MtF(&KLC4H03j{LFP)q# zm0GjYqCrPb!^+I87?IwXn@D?{Mr(P2ankTfbL2b6!_x?U`PbI)8Yx_T)bzbc+};aq zGSbQY7ikgFY`` zZ=;`LO%CrY978thaZR%re`_Jr9T4NKQ7mHF$D!w}{!&;LCh+>-QZm`vg6cw=^ihI> zyv4*!0ZGANPx@5~Q)2obi>%s-YO10|r!ZY|XWdRs2hg!H zFmSjBY@k4P)16{kI{(Dc!7PGb^UzxfK{t;MOcPjZoQQ19p#*a^>lj}OxC)E^UVjo^xhYJT;vw4|IieZbxGI ztAjN4r=MN9vTMvRb_A_2q&wvXjofSG$bTxf8j(Xb{FG_&@MTKVqEvZZ6Md-P zTjwe#$F1wrtWQ6l?t5$L*68iTs@0(?Xf<{h z1_4y-ElbIAom@4dHp1QINebnjCkq0edyC=lk&Q0P<-6On>>9~aJx^Iwkn$1*g}C#> zyr6}}!zSNMDC|{E>-I}3fs(T`b+hxNqjg^h2~JLwzyiFL6Lq}E@CC%peIWYHF3Exl zqeT|8vv>N-hV>~clfo75!bOUbD)odBF8dJ+56X_@reX$!mUuN;qqPcDrBgz3jyN#r z)J56k6ce(s=X>SP)_(XfA+p@%4i5v>6EvvLQNi^zYUhwgP+V+Fw?;llcStHvu4s7V zoJXG#4=-Vqtva}sru_}Gh41IxT_1+uDQCgGJ9DKWb}L&mj_^>;XI7jRolc#nKMEUJ zUP!iGhUJ#7Dv#%DW*@#pC>y4vQAIdyW4%>WgnwVola`*M8M?4H){jRmq#k-GlS0Si zv%WoR;=+@EwNv7&b3v$)zbushd4E+O>)Ba%c(GvbuZ-gd)a?IM&^%KkZgxS`Z@+4UJ^}b#$W5VTu-(E?)SUxYd;InlJb}UNmod2$| zbDh>n>>K)EKwWGc`5jaSsUe%m3&{)J`SHV!z7Praov0{Re3L{!szB>v?Ll9Q#(W=iUOb1^pEapS=YzCJU zAm93S6m*uoH^g?!!9;%b;$Q${%w%#=BGUkFOvK7L}K3gri{Nmm(*SZt7aFNCuqKdnM~P@G-lqqd5$ zl6RQ--SMZy%~w3DRZ`4CK#%5GeubLN?+Vh0sUE(=5A6Pf(|@Y{24*EJYsw~e(A3Qh znP-@Ck}-Q0T?7T534)XZMCjy_#;jEQ;Rml_mWmYJD`Qyx&LP4kCo%hwG|vjApB#e{ z%>s@eYYNxP2}wkWag^5CrrkcQaBXW1mHRj#n_O{w5qy=dJ@A?)dk@LVcajVpOCOt% z12l!WnVs1-^)9)u>>k?`hYyYEczpS8$FpY#!R|&uKD`VKmqXIR*2zWbH)UvxR&8pm z(2QjR&l@Mkwb1HmwI)Z%B)_xt$rTJ#Z*qs2pd+$3!?hEwFor%ZGe+>o)}t_TgOYbp z0K3MChO^wK|IV=wS#NKqomYAOfOPUzKDR|$JI$IfBUn2TVsi2Upg+R(obto&*@ogO zs?55Lm!i_K9Sm|JE6MLb{i|L!v5W#kNsD*6DQ9K-z{PGRhZ$A;}Lx#EJ#(&;L5>!eL$Bc zdDl7tfYPgx99){JFC*1&1adLr5=DW$=YyG_@zH`)tl+6F51wJVTluue$@N7|b(;T8 z@0-}?-5U5dy^=IGvdWKMA&>@(2gn-SO%gBDmHY(>#*^CW-T5gV(|?O`SGD-oo26Lz zX>#!6NO0Ti#)RY%iLBDLmaupe&(b*Se-36szlBny*bDcine+;S8l#WzD!@#rd@K2A zAF+CV@=g*ZD=q=N!5UTp6{cZMXWyLeM0{VP57*UBx4l$(N?2lWTN2oaV854)?~M@_kOI3aAw|zAr32X zaQqvo>>49{0F6hM5SyAh%^}N5%$3e1 zx&?D{-vq(lQz!8!e&qa!>^MtXC$jwDgalex@$N{(`M03!u6xWQW4Ye zRy_bx^hre^B&(4gzn!xD!Pb=A@;7CcLDy4M*NTX}V1%nGOmj4G;9cZvLDBfPRJR2e zM;D5tji4aw=KR(%NCzSpKAP41Sf-q3x^qYN+Eg`&iJyAq(Uyw(D=om4wW# zgHpd{+tSJ_Qf4&Qx035X&i6Yv%n|)UOHLAZ2V|@)7QL%vc~Qh)&VEFEoM>Wy!7>>_ zlr>6BY=xoKj53Q+fgVk&yxJ%X;*G^Q_+(kk0`f?^;4zvJ4LE%YUu=D=BH3I(jvydd{@+bru&MMuOQ~#1d zE_h0hhvWXO#RCNO?OZynTPJ3UCld$sI!z@GdhqyYQ!1(Bxa6&8=^^f%Z~VuOd(_U+ zvaP<^hj&SlAYRs?FecQsHL`DHWYWwXq6Ylb#d9%xL8uF#qYU^?eZPRH4mDlZdr)i- z5hWq48vlt_OKO$lP(*TfI`6#t>0wsI{e~iTJP_tik(GI=-f1W;R4lLegJA(x_MgiA zwJ*IEOEtn0Hm+UsS=_qwLuSI(7R&Jfa?dY@1VbcOUb=ufFssQ4YTYJGx|j4Z8Pz@BX~`a!$G0-C-hRbtjZ>s7BgMT~^fahUPP^XS zSRk>kU_RoF9yX245sFLn$h^thA>Df2jl#|JnraM7aw`_LH8?JxV9_=Gd9+HWd-!#$ z+=a-UQtRfy&&`r5a*wov)6TQorWNT4$NB9PBEmm zur^9?k+9MxQp9kZlEpV2M>OerUM6^#UZ=73kh*IbKCaOWPAjL!=*IZ7M^7cy&VJqbb7@h(NsU4oh`OM4<+JTYae$> zPf%TWc0MlM5OV?#^UXEDJ-uh4{CZNfTx}Wj*-|}tfbUF?M^*)XiAZoVmrYI_9u?piIDPaCua5xs zD4S|X158r;ZAfoDeH_bEq;he6r0iuA3)Hr-2@`^OjsCPr&mWR7+s8Z zEX?+3Q(#AawN{8Kwj!Qr??>Pw50iiJ6K7{cOZ;^DPQ_u1pz}!l5?{ST6Nj(FnTKf9iOYa-T-T8co9NbhmR+NVUo&a*7HgF_BFSQj*u_;L5uUr)9Li$ z)rBICd8X(`6rwhq%~eBcQRNMdTP<65s);bA(`heLy@linv{r<0UfdIGTfjhzq$8t= zN6Y2x=VA0=Y2c6?m(P=*FK6tlYk^ZvHZsvWGmgn-OAM6Bq%!YS9pc^L_1S&EPvxq4T9u)^3i*FVhv}jK0 zC-Q3%d$38$)mV9Kr4_}3O|~1Y(X*Xrru0381gI!bL=uAdyZP%;PL;;soxFTSmF88$ zVRs)T&kMXynXH~f@&2sV7?jUan?1C+)SvfbqcZY^{3xp0%SIqUjccl`ubgJ}YiE73 zP({&OsS!d)oYo>UR+OwDFMNN_g|}G;L-A8iS`lVRLuw_ z>UA0dN4X`oL(P}l+s&ynxer$lb@dWgwYd2ObK{v0aphG+-w8IW5EnWAa-st=s7js1 zvUzNIS-@e1sb=6WGNc$iT%aCaX z&18}<%yWP7Im?v=P;XxMOWvp#S5Xhpv%eP2Em(>ns*4guuf)^GuKUC7{2p((IT5Yj zC^YfZnP-I$c&F7wh_TU|knk0a>@rx#n*xX5z9MMO8D3HooaVp8^?7;ma01rDeqrT# zbS7oqHpyqc`m%UK?)pP5hE)(TK3pH@bLfWwDY3RNx{%4ZP* z!!lMla}LaB>@b^O^lM2cTg3w%oNpvRn?x)sC$QbT9C#=CmNK3CGV&wO{2Q-z<0Q_4 z&r88~OgJ=-JaeDKHv}?k*#B@;0!A78^tI&qp%AL2h!W=yep;FnLw=_Dqq#*FJS(GF zQ2iiA*GOXdEysjs4pbGi`8Ss@EtZtr>F60D975z*7DkbWZSHJ#@4Nr@!+0_g>-;z? z#I^R@7{wP+u2~veDXoA}_((>hMuY=S$J6qMa%j+%186`q^J1{p=L9J@=e|oP+K6{0 zd8Vo_=!z1Prz+LsbAVJe_OTQ69fFk+I2U~V_@6UazYPx+K~JwJ(oKHa{O*T1SUdlf zfLJxvTPx~(+g`lYWP+jXFJI^oG20d{N~G>}!KcHX@ZmJu-RaOucus}=P)9>CXN)H1}Rsty!ONtCYscZQv zEad;9R>fdXHG06b#W4(rG^ree4B#m~M1D*d_Cw08%uer!nTcQ<;GO;I$ zUp`1%;$%h^IIoOW1l>)RE<`8{7Morj#yY-3CA17aS-{S6KFhz`9v;p(7`+pz1LdfI z47wLDE63(S)w|hYJEctStzT~+CZW#0P?Yj70eYOj^%GM3FK@}fl zZ$>4cy!;wk3p{2 zdp77WFeA>Gmkn&*gMh!-q+9|Tl-?l2F8Pa(3H{(`O#wm zoD92Fl>gwB-7>WtE;@pFKN_f@xj=-Kr1ZwbMH^StC!+FLv5FU*z+z~&hG3{7+DVTq z(Rs(w!9jKbH=WWl@YyDl^zT(6G(bMgE2*Nnkcr`xGzo*TE~q z^jz1lgyh|zfre9;E2X&W?fg=(OZ8m_jQ>XLc2FXtPO#$K0`_Y8`mOJcFQUE6W9apH zBdlSf>*hC}{@$5y@HGd5sX15RZauBbi85elVt!-dc0&=D#`7rJFjtk^9cq^>81GpD zHs=ex@0JN1b(c``1L<7qywB!Q{z4bqggFm@2a{7bch2Eb3R6Ny8_FncpZG3a}>@drAv=4R#9c{WJXR$guyXzF< zZoX-sBi=j9$iI%epqk7df!z!>@4#Xw#BFa^y6zHbtVl*9GyyMVwSsQpR9m7LV%IrO zN>>nmiXS)`T)aS77-@ByQl5X8)hA$E&`hoQlUkpo3z@8|w%m+5!@F)LQ{W>BuN&RE z?ue^A*3-Di$~c%~4D(#?R`5tuxiVt&FH9<;)|2$AKOe2r4rf)y6C@LoO3Z0td!Ue_ z5B-;tpd2g`n4<*L{}h>@lQ$+svcJ=L>W#hu>|@vhW7@wuH$C(a348^sjv?7K=o|I3 z2n{>6O}KSi57N9|=EH{Om4R)pM;JiHxr=UJZ~^dRi!7oLziDXQ?0=%(kvbE`|ITti zq*e*3q2VzRl36IgStt(T6`v#=$X7h~w<2U;*ar%|^gX&1o5>*g3KRd^U#{kcKlB^g z(l!EMH;27VtQ95(cfZ;dP;ColWQ~mISukMH#I-qF`rX5+6hNO+;t$u41yl6wO}PBk z%wOlp^CTz^R;CEoAgctgx4t?tUI{X&F=KNTGDc@xs8N9*Zv^w2|L}1LJ_&j|M|!rM zXTH=mIpe5++N=oL-RL^FQVpInlL^A$2@M8lwoKy#2$Q|k5u`u&z#4*zki|xFU~2nI z&~+=hCr%+_rg74UH+F9tAA425a-73vsc0axs}R--&XLsGgqv~2G2jF>YRP9d>^e+`@sn|yHbAX@rG5t2 z?V3$W$X^X@&GIs+ZK08-HTf@%aha4yES-Ls%M89gl}F!M?+U8-ze(mNI$Ev>R!u-* zEQZ!RJ1xa3Y`433%J5_5=g;mP6v7bd3iGbZev8=de4L@a7J0)$93p%KX;m8#a@P4ty< z_{hzs=DFSz1O9tP1@^V{fanTIR!?d45p{)Cz^WTl|_vuV*gK{hvxxXKgW34mvjPyR0O^K~CfR*xxHy@bFgJSNP((7-hAxDlNWJhTis;{ODT750*BNSF-sa)+m zX9UZxLG$Qb)@24znCZZ>6UAxo`P0^A z#rhojvxSGAtL59PYw<)KzY)=`Q)`7x4S$&xmByasx!ANLZnY8OWKyX8N_J`68|W-GkN~n7%{52S z43hcu2J7umQI!_4k~n7&4p1SU$j6>VUvWa$I{exTzXjNn29vHzyfBP2K~+ZHGyGlx zM2NPMwhN2SRZRX19v7J{TF(C6CtTSFQw57f5QsCYGCP;CJ{yYP{-+G)c6q;Yw-K!uO3{rh=wqR zF9n7;*%SiS5ZQJfS>rJxplDG5I;7q8o6MlS<|?sAbhKDyL^W&Ws&HKenLh+IfMdu& zTf^jvZot&1o*#D-1STkWvWra?#jsYaTj~A~$Vvg324oO~2C%;a+KiYhqs!N>;*Y<~ zDAUQYDW_(dVbrUB8)_7WmP?5_nzZGz;&j7j38XHm0R$c=P&6W6jlXgE%}cYkmN}}U zPX9_D;w=$K<-8u9$oo&w{!IIQkQJAfnEePR#=vrspZM`VLF?_`jVYo`*cH0QDdx|~ zIM@+1aU5W%-4bCZL+C;&56Wz&KO=LjPyVBw;A<&sN8Q=& zEfn_3`w+8&tv1{+u8%cL&ELS8Bt>|`Y4peVWwI6YJJ%MH9obJiyq+F-TXiuNxd6e9 zTJnGz1rR8?jP*4oJ^eY2Y{7`(i-J!yge@5~DGK+Lk4qfOTzC~);P*89OBQS{>{l98 zF_zFqwfcj+bl6x`;o+mgN6n1iirc>qU~cU_l=_f5O|x`h^QP7MOC@HEfa0)7fOd=@ z6Bf;vBFGs|pJb04e}hqhn1ll3{*ad?(^r>})qJ5x57oNf>LT>5I62*R23YiMGxR*n zF6!M@txnj2*se;QArT{}4>DiY@76xRo@Y4MLSkR^>}Ou@p15z zD|Yuw=9?G4p|B?(CBv!&Fuzlo7(!Du46F)OuB%Y!o1 z8@y6`7|d~d4JTE{$Gp-r20IF~6_xwBrgjl`@i)8uMY@`ACiH9{S3UBt$8t}(srWSR zUK4;bKk@ePXqD8;1=`gI-&HL(_FUQZ^v#e)`R10iLFPj}0nB&&eiPx!rTXn%M%zk;=lU}}1Yn*8>;G-br zZ>kNpQ|Y6-%V`;)J5daG(K^2v^&Pz=Ar;q@$aF%09RJsqNTi>jTol=^CUTSzcm>hJ>|>_T8kIE$Q9ps zG`zqr7VHr}ERiqLzv!CdI-NhHyW+a#|0d82I=r89vG@9k=fK7M{?FGF-{YYxzXVpB zwmWUTQl1Y;p>R|^kcpwb2QDTJ*9vAO&(xONUXQT&h< zx-zl2Ycadc)4b`x^uPhj91#YmAc}g4HdoSShvXsu&633+a$AOCu#9g)U#hspS%1mz zW>cR^`3d#g9~nDcmuE^QCNpWy!}E25N4ppOqjZn#&FQV40SJI>*rV0eD+OhADi!%w z_xT%3Vyhm3u?vSXS4n6~e4l%E?76c6jp~`|nKx7Q>v?*id{aT=X}!u{8`k&NyS^xc0avQZg>o4@yPXG#n*7JXenlb7bMjYd z4%>W0FAC;mx(Q7YsqG#7qoX&)M+2lfj!{5HcQ0uc0Qz`;rK>q?Ogr# ziD~3i?uoUDyGb$|o7<6fS|h{l^XPpvXs?~>(JsAjjzfs~wT79oMr#{q?>cAsxgyiXx-n-*ewDy4ZAcAxj&$Kz_F4b4^RIx z2=4i8^hamRiT7wCK@U>@47ywK2Q8wZEx1M*!Vl_CI$O zx@lXOJ=>&HMH-&i0uJT5)$}dC@o_ZZ_oTkVm@dL$nyRU z=dGPscN|O1nZuq`5Wx>!mJ5^tO+M_oq?=2`c#f6_Xua?6m{?cuwxLXG-A6~n_t86y ZrjobB@s5`tfeS!L%JLd=)iU6){{_?5;TZq` diff --git a/apps/meteor/public/images/mentions-upsell-modal.png b/apps/meteor/public/images/mentions-upsell-modal.png deleted file mode 100644 index a71b3b837d9a77866e4b2c490543b1f608050871..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9723 zcmc(Fc{r5q+y5;>mZSyQpA^bk*|Me*(Gaq)W#6(6S!R$2l|3>s_O-0p34=tq$MgP<-@oti{xQcK_cix*-`90spYuFF=jWVf_Y8G8+4$K20N}i% zcgq9-jyM7U1Hj5mA2}V;&rAP0=BH;B2mtJ-4nGV)b`BqXkRi}S_XbciEVx9!U~hXO4jsqYOB=VW0t-1b`VwIP&@Kk4Hc6z1-yZ5Q+FMxKBzz>{F35YtNd-$D;hi1&pQ|AHTI+9tL&7=})cecN(UUGV&us(F*(O7r@4tjs?8^!(X z80jx0Cog@Cp94SOZ&Y!dPCkg7x-m(kV&e~7zt>R9-ukadCi(PZ4UP7(Z8&dt4I`Z^XK=COacXFGS} z?8DQ)xA5OH<#}NZe$|1O43aqnLLcq_xi>*bVE@N^FNIcM<$Ilur=3lr&3bRskHH!l z*|o{{pJfdpbYm&;Zh!ySb9B~6_aCe9MztwzeJoAWK8s#^Mmno$U;Kc+rF;V~X(?WN zk=xow0DzDvt_J{7dE8M9^b2o4hHuVSxM8hIY$own+LfMo{hD1AMQ`N<7K?rfjG-?& zaJ6APR$aLj7_83H|DI!X+ZO&o@B8}>s}H()0-E>*hWVYdg4v?2m&Yg#$BWE7JT%dGe-V$Ypv=81CCi@hOim=_B?>jZ zZsxJXLJAF&^DpF31bI#T04stFQrZJ>{hl7L3T-V=jfMd(&=-%8WhJlgO3C=n6DaP> zU?G+2h_k;)@6M%9L}X?9&U7_2il-;cZV(9PVG}`TP9^pgn^DIa3707j*p_V%P&u!| zBSS-3d)?{0(ozx$(Q;SH9+KyEP(1qZ?URLkas-5kWavyuXna3T>YIDz{?eR}d0_R6 zj`dOvW?QAzaNpW*D+?Q(9j*8_&;?>JCD{$09wYpu2ZLDWhy9Rd+e_Q(n zo_VNO9Y5(-$sYR1kKvWg)t9Z;@)KK?;?r7VLmT2!)RiNKhZsLCUsX}~?HS&F#iC8W z&@T%IFC~oGS-qb44hl1pmFa;0p8)u(od2U2mjJ*qLB-Hnb*kFR9xe(#T{0x5cIY^7 z^-eH7RQuyRw+};Y)!h3n3noVZ=)QE7r7YmD-{v{XKu<+)pR+GpUtyyMKTDU>lOl+F zjE9l=>Jj<>4g-~ol6qx@bgM3=u~$l-{2dt*0S@|v3miw5%Zo%0A7;J@A${Qe_pDpS zA0-cCiKdRfbn~C{`13;j+3OM!JoEI|m>+v`-CLKFzKZL}*9?q;A;H1oB9Z&2wj!zP zrb|d(roN?Nwe?i4>BxNY&y+3;V9G^I)F&@-+`!iI-m&X%-0~@LWCF{YGL7-qgpGq# z7jf~!LZl<>oJ!h~Hv)B;o}O+$&;ytqU#(HoLbxM-oa97k?&WKWp=<0;&5k510>xZ> zv#*V4yYoyAlBL8O@KOdF4B~i6(cy3k?Lh3v{ZY;6sOrYufnYick`I&4|-caVAM|*9K-GfTOWT&)y+>|#K^JTdSZh;-{Z4>V*TfB z4=F?J-$kFPu^tQsvbzZR%6&;uyU?1I(1B6IQb`F{1kSb1KpN*Iq z;(a~pE{zRY_WDU6JZECiIOSa;C9J+jb|_;Y-UraJ+K!a_r#nzZO+HPp8x(+>ZHr4< zsbvcLt3~|wF||{^B!;#haRRIe>9X{S8R!&NzE)yZ7287Vd>&%M@my&jR6_Y!^Ku2t zN3TajFDCnB)-h{_>Mu;*MGoij+ik=z2}SNr8Z?)y$@AmE(-OX#Ddt+s9=(;TFqE}7 z(n4I?I;3daC{H-u$H8cMj&B-&PUa#gL0nHTgW0xNxy$JN)QqhCLAZEUM~0FO^vl;1 z#xczf(z^4RD3mFKcah997e(vIkCqdIucv}GjIFMg96w&Es%vJAd9$5jksD5Q*Rft; z3+TclR}JbK6d3r9yONvp(>84)U3WqPc011F<+8983z{Q;M&}1I6LwELi%gZP6=u8# z^6CNFAD1#%nq76`A!i)DW7g-bUwLVy!f;?JHS?=x9KgYn#)WWoAZIKk)->6N7K^;= zTj4FEg`*MZ#1iSPE<8)GTIXoc7P@S}`&MDFpK^+v*Z7Ms1LBZrhpyO<*aK98LL+8- zp%>cQbHv5N+(%9gW^Y5TuC;ro40QX80XNsl>ur!T5g_OuY)t%<#9)WzkJWYgDFv11 zNQ4Kpi@MJkfa~PSwwCAoT)&`qHTDAu>N4(~_Xw`Bpw1h;5|wTj)|fG3ROJ)|WqVwA9V5^A z9s>$GyQ2;FnH(2$Cp<6a=G};?eQ4^O$E<<(NG;R8_lmuHs|UvHJ+lP5G9#ZG!)$es zUi++nOrA272fYFzCoTALRw=k=D+?B?ghBWp=Sq9x2szfKWu~BySSmltAGpq9>+UH* z-)y4;EV~Sp!e8LrW1(b|IVVfiC+X=}Pe9GeG`FP42>q`hi7Hj_M}c0Y|T5c4(K4j?G^FZ)xG z@7NIc-*OyDP_%2%IzUz~69S8`gcKGlH$Wd8bmdLuY?H8(K-BKLY?jSb*6tCh)r?1X z>qnDY;IstAo0|zp10mut;R|u#CFzWajSS6nMG&j)Mx1NvlY^Q9z;xD`1&#)82EH&e zj=x_>t6-FfzqU4=59;4E_5V~GVW~xcJRNjnuw>IpajjLx%%kS~1ZN>k(?uivkpx(u&(uJA* zevZMqk=k}GcF(}IyZc0Lem)#5`h(dnL%?rZvQaCdkeQ=%Ky?7#Eyek`hBkqO8}9R$ zNN0*36y-$jM(Y$bd;daZZEWYRNYLhCDAA>2R|n3rpz}W!rp-%0Nv?5}FF5Lhu;ZLR z>$ewEvc#l2^T;KYMB^9Ysrty$o`O4jD*+Boi&=76kb3dHdWHC(7`HBMnO9;#GVS>M za^zo*jOz!)uWJvOR8*?jG|yT@3Vbng^xQS_YOa~t6G*t$I#zHCPn-mYG5g4VMVOB^ zI9SUVmo(jXNiW;Q#WgY;t$WmnU84=?dI`<*dQyYAd!G%ujtd#D?-zE-fRl8^+HjdR zKPMG=ZnUH^+vD+Qki16jkf+EV-Y8Nh>LyC3s{ATRZG8Q?xE{gKtGMyDB%IYOMx68G zIlFrN3S9g;sx)4qd(66ej&Sl|{|x8s7_A{|89ayDgkxLuA~D`i4+u9&%Sd%Gz>!ht zs8usx3y8Jb@5H3wUcVUaqcmp*=(+VumrM4B{%rwIpnNE0l0ggJ2HmmDu90RE5}@D| zzFkZ&hpeZXFHy>N&ZbP45kEF>LgXlgtc)s}vK@PyYe}C7Q+<*g=UJ89#DsDmALOwJ z)K)K^%iWs$sS7Ox`>C=~`jv}XzJpc;aRxt?ACGTIoezeMlP9W=OkjTO#E(9E zc<^pX(rCSelEHOAPSK4%|4gn|^NlZL*={SgJnv+|VEkH_5n!|QJ@x>HT5#2q^M4@8 z5M?ktZ2uyhI+_A+D~#%}@r6`JQ1@t+z|9e7VfkEDUAGS{*8qB1>6c+xaR5)z^w#u_ zPF!Fa;suT;@XOD#MjW(=zz#{q)=MpF#jFZtx@$X=fUjv>+Kln=gMy+8-Ax#8(`z@5 zRHb$*o#tS_k+a8{jBm0EPSnZj+@k-2_hzqQMF4{QbS5WZcGOCha-@gXQQ4=Xp_3s6 z;D~Cd7hRcN4=X3ykhHlY7>5&Y=pwg6%qYzp%Rn zQ0t676D34=Z9i67Q)<4dpVmoCGRf|0^k&usAGItm7Uf;n=0W}) zNr}+EqwADS>??jd&Qb@R6}+_HzX&Hwa+F-;L1TS%zz$2KSy8Q) z1dwcWTBnR>kd7@ng^uCwSW^r@InlJAG%gqnqK%rBZ_=qbU!wH>^DZi9DA7&JL{;CUJiPTs56fNA;MHgGxY(|} zbdRWD@p2Jr!M;aQ7PA*xB+R*~i`lcd9s3Z9CVmQPEW+eHLiJ@R`3$4Vh9cz4m%O&! zE_be|t3GwyQlr@c1;-8-SkPo`wXrkTNxT1|;cDAff|f-2dSq>=EeYC-RG0WM9r8tr zOZ^qIw(^}<%=awDq8499k}6M@L}xMU^2>V^lxkchOS&(2c)N5x(b4 zJhG$cWIY8cG{enxC&~%(Y1Q$Rt0dtEzfxrXe{qBnpii$kM*PDmb{7BP)hsV)%$sC z?|7HFf>3D&jJ-=0%x79ck$=LDh8 zo3xc*HD;9)GD?$TXJX)h84h}pes&y!sLw%(@%F&+?W~`%FePD_;!S4X)(>BwyU?>; z3S@T-VdZ0G%rnTqCu^5Db%?s-ve&}>SJ&OG1kUhy7B|XS-e$b8%2At(Z%9j;U)u9X&QXTfZbyPqjr! zP~je$UyYN>c%-X|Y-5vE(gpmT6qyBG5W5(6lP@fI0+}tstBpB#9pwn(Wk74XW;Zi? zZ8wH)DWKPv!zRDJsdrKsY2S3}a=!f_udHwMj>w{YbD1CXQEkN4;^0=K>h!YGw3hww z(aiP4dvv-E z6L)cA3p$){2mR%8N=cfHnG?h-z`&8xFHR}78Z|=eO)0Nv@6i3@EVS`SCp2o?H5z89 z;lD-QdqoK{l&6epGho0+7UP%|R2w?`Ssb@*uR1}$7Bkzca_aCjE;-Q|w2t)y+3MG+ znbIizfS&x8%n5an6npJ^d1(z@>bn=`?Mrc?(1TYNVRtim=s?gO8GWkk31l)GO~Rz$ zPZgK%)pmLLzzok620}f`!>X&Fvr|42e0^>Yrc?}~9M@ypBhPWZo@vW=y6c5em>n=8 zSCEz#^*C9Z7Gw^#M9T5uU%yt#Oq4|E(}Bt`=&tatvC|tqRv;lFSuJvEtx9uh^~Lh+(PNJH5O8LTLq`IwX6zEj5Vjca z{Y`ZK)!fB$Bj}GYfAJ0L-N13EwOl97Av^m@YtPy>Nr*-n?OhA<+Hl06n6|m0-<@Ef=%hWIRhUohMei@A&dD zk7Y2+QUM;bangp(tryYjR$a1~nn5oSg)TQCGd@I5t`#1f{I+snGz%o%e?bjf^d@@Q+8ZvjK=i`V}ZX|`8wm8m)!YpvWk;N#=Roxae9WZJvO|pgi@5K z)!5vyrZtFvDjxaS=fu8HDSf-Bbe)Sq#LN=^KD-*%I+99n7F3l4`h(nd`;b>o8c^2Z6VbDPRh1H*S;(rdb&3f_bkCjNLFL=$Fc z70tOT7vhZKikBzY7zLGyuo(;9IgQb>W*ArKO4NO@tZX%OZnDmV*A*<}!qIo0HR4!I z)OC)}7a@CSaf#MGClt&(ys3+bfECsjSoa7*JL3~AFb;~$Wt{V`&uBhlln{@ECr#)!TMK;;l zV}arSt41w~YX6p;7L89&X&uQql&}8qobF4}jJX?Di2{du7VD8ZAw{~e{}B+)N4ZlI ze@pmjvY&j^{;h6(qpL)}rTVx1{}d(vc_5%MMPIbX!=^4UPrK)KcKb*k7qA$g{3Ixh zpRKF+J`J(HSs3PQjf05|Nm(0avqkjtuFR#ojdD-L$e9tJz3DOKYxFAQIAX1m61xCX zs%=4B7O{aY6(zTd0WgEDZg z$HrG`61c>7#6y%vXU&bLpD#If!3e8{khA<&E6-Zx>#}{4_tz#JDK81^KZ9IpgS+i> zQgmJXKlmwH|Aqy?H^GG@uwyHvO2;NQCbVMnrpss&fNZ$V~yH+wGC_mUizpOk@AtJw*_^Twht3N@bA8EDQk>CwQROZ3w z+P2Z{OFJ*06P!O_U3mH&pfYqV4|&-o<;=PRTV(hJ{0m!q$@cQO-HLEbW33*z%g;zj zOKZyB>EfN*72<3C5Y$eUubgH*RJcEqh`^kcq-D_Npt!xzYj*vSp_9D2YqHPJnX4#LG@6 zR7=u6+#>3}WJX#Ys`$#h^TH1qzTp)yCbQ{6EmKdGu^;HhJ%tkyVmp-f|1=(1yQ4Jb z*f0BaNYY5f2HY5gps_@PKUX29-E(oHjIswAvdb?HJK?XjI5U#Cj693$$`8CpUA;xb za@i%?=xR?^rB{mFV4m9HKWJU3L8g8E-YnNaEaLcc-{o6&KNFmWZ2p9c!??^g#Ni$$ zMxo8T#(EY5gK0 zJo9^?iF&ZiXayl4GQ%Y!V)70(_=PRM8G5}M3k^ySH5Wbp)}k;7N6FHe|+=WCCJ*=o>-fgj1&#VHXIjlE*6h?EL`iCPsGbv951aR z8~nw|eKFkW%^+8O9+!D-v|jsG{{4Kdd>)*GBn<+KOxs~B(I(1K>v8H{#M}Ked*tb+ zZ!_fA9T;x}DVLrTgG7^0UCX~av_8M--ULyCSI@P{bC|RHEnm51EC~MwYQdG4=s9JN zuNNCzzO&oggZO5d=j3TAb;*rc=iF4=Uk<(UKz7zqWhdb>^$}Eiudv0@j|yu$Ln>5)5B-*_wu1_ z3zIZ^QfP%I117lIM5QJ{-8gt?Y{L>$51G;gqr7Znm%lrW%HrQ_3;KjrEpN%0&<=DY zX;Z>dnzA>Da#JDcf5GSu;X<0YW;EXHlj4FO&wg!3XR7iRBxf;S{X@X26xcD!|0WCF z!++?Mw=eWQljB}^fz?6CCoBSaADcJ!R2Yewp1ojBs$5lvH)*;r; zxar}r4`zb&B_inhjEZrGjYLGQg_M?1Y!bO*QR*W3a~!SoZGMJ2!pFv>9$DrkrS=GQ z8NQ1N22Jf^aXDst*^%BT%F9&=6Da6TQeVkANLtt#BXA8EwGOW-9VRMkG!HB{uC`%! z1pc)-MtXLSIv_lS15fUL_YFT@ zIo$Myd|$&p(;m}iCqo0T^1tOwQ_dPbnWlQRD?`|R^Kow2*gJ5O85lkF%Pr(i~hMK>F&f|AuTD%3|B?Of1-5{fksrs}|3;leoV z^cMCsj=?KVcW^&!Fzn#|hry zK(j%$qsb$-;JZf6B{t(yEz(Io=sGWA zZFq9cOmVYN{?~6U!y4iO)EUL<3JAQ+3UAx%10FWR@4%Qz3Xs+IOD#8LX?81E7feOq zyYrW|l(xlB9nfh()Cc*?d&xZ+?715sWQ%K+0>^hQ%a$z%u8^`rgd&@~CP97Rol+0Y zCAG+=J5!$`u)FFRw1cIDd(;-67gLehbY`1Jkmw$lH( zA!IhNeYv@w5q^&v{XuQGgE$ylXP;WJHxwM68))pD%u@K%5K6`D; z{&t9FQ2XdwKm-N~YF7wpBI~2J!MWye+|B0Pge@Z?wpj}o*+L)6l)_=tN}mo(ph^fk z7=o%xCcQc}mZMotZ~ZmkR5^ouj`XUBu1soz`y20(c$O From 79f6388678aa1241628656792e8a03565107a037 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 19 Oct 2023 16:57:17 -0300 Subject: [PATCH 32/38] test: add cleanup to users tests (#30654) --- apps/meteor/tests/data/api-data.js | 9 +- apps/meteor/tests/data/custom-fields.js | 18 +- apps/meteor/tests/data/users.helper.js | 17 +- apps/meteor/tests/end-to-end/api/01-users.js | 466 ++++++++++-------- .../tests/end-to-end/api/10-subscriptions.js | 12 +- 5 files changed, 275 insertions(+), 247 deletions(-) diff --git a/apps/meteor/tests/data/api-data.js b/apps/meteor/tests/data/api-data.js index d08e4cc50c54..b311af16e764 100644 --- a/apps/meteor/tests/data/api-data.js +++ b/apps/meteor/tests/data/api-data.js @@ -13,10 +13,10 @@ export function wait(cb, time) { return () => setTimeout(cb, time); } -export const apiUsername = `api${username}`; -export const apiEmail = `api${email}`; -export const apiPublicChannelName = `api${publicChannelName}`; -export const apiPrivateChannelName = `api${privateChannelName}`; +export const apiUsername = `api${username}-${Date.now()}`; +export const apiEmail = `api${email}-${Date.now()}`; +export const apiPublicChannelName = `api${publicChannelName}-${Date.now()}`; +export const apiPrivateChannelName = `api${privateChannelName}-${Date.now()}`; export const apiRoleNameUsers = `api${roleNameUsers}`; export const apiRoleNameSubscriptions = `api${roleNameSubscriptions}`; @@ -25,7 +25,6 @@ export const apiRoleScopeSubscriptions = `${roleScopeSubscriptions}`; export const apiRoleDescription = `api${roleDescription}`; export const reservedWords = ['admin', 'administrator', 'system', 'user']; -export const targetUser = {}; export const channel = {}; export const group = {}; export const message = {}; diff --git a/apps/meteor/tests/data/custom-fields.js b/apps/meteor/tests/data/custom-fields.js index 2509dddf5d84..e2e175429b4c 100644 --- a/apps/meteor/tests/data/custom-fields.js +++ b/apps/meteor/tests/data/custom-fields.js @@ -1,4 +1,4 @@ -import { getCredentials, request, api, credentials } from './api-data.js'; +import { credentials, request, api } from './api-data.js'; export const customFieldText = { type: 'text', @@ -7,18 +7,12 @@ export const customFieldText = { maxLength: 10, }; -export function setCustomFields(customFields, done) { - getCredentials((error) => { - if (error) { - return done(error); - } +export function setCustomFields(customFields) { + const stringified = customFields ? JSON.stringify(customFields) : ''; - const stringified = customFields ? JSON.stringify(customFields) : ''; - - request.post(api('settings/Accounts_CustomFields')).set(credentials).send({ value: stringified }).expect(200).end(done); - }); + return request.post(api('settings/Accounts_CustomFields')).set(credentials).send({ value: stringified }).expect(200); } -export function clearCustomFields(done = () => {}) { - setCustomFields(null, done); +export function clearCustomFields() { + return setCustomFields(null); } diff --git a/apps/meteor/tests/data/users.helper.js b/apps/meteor/tests/data/users.helper.js index 92425902cb5b..82ab8446547d 100644 --- a/apps/meteor/tests/data/users.helper.js +++ b/apps/meteor/tests/data/users.helper.js @@ -33,16 +33,13 @@ export const login = (username, password) => }); }); -export const deleteUser = (user) => - new Promise((resolve) => { - request - .post(api('users.delete')) - .set(credentials) - .send({ - userId: user._id, - }) - .end(resolve); - }); +export const deleteUser = async (user) => + request + .post(api('users.delete')) + .set(credentials) + .send({ + userId: user._id, + }); export const getUserByUsername = (username) => new Promise((resolve) => { diff --git a/apps/meteor/tests/end-to-end/api/01-users.js b/apps/meteor/tests/end-to-end/api/01-users.js index b8343dc015da..eaafc97527a3 100644 --- a/apps/meteor/tests/end-to-end/api/01-users.js +++ b/apps/meteor/tests/end-to-end/api/01-users.js @@ -5,23 +5,12 @@ import { expect } from 'chai'; import { after, afterEach, before, beforeEach, describe, it } from 'mocha'; import { sleep } from '../../../lib/utils/sleep'; -import { - getCredentials, - api, - request, - credentials, - apiEmail, - apiUsername, - targetUser, - log, - wait, - reservedWords, -} from '../../data/api-data.js'; +import { getCredentials, api, request, credentials, apiEmail, apiUsername, log, wait, reservedWords } from '../../data/api-data.js'; import { MAX_BIO_LENGTH, MAX_NICKNAME_LENGTH } from '../../data/constants.ts'; import { customFieldText, clearCustomFields, setCustomFields } from '../../data/custom-fields.js'; import { imgURL } from '../../data/interactions'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; -import { createRoom } from '../../data/rooms.helper'; +import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { adminEmail, preferences, password, adminUsername } from '../../data/user'; import { createUser, login, deleteUser, getUserStatus, getUserByUsername } from '../../data/users.helper.js'; @@ -39,11 +28,48 @@ async function joinChannel(userCredentials, roomId) { }); } +const targetUser = {}; + describe('[Users]', function () { this.retries(0); before((done) => getCredentials(done)); + before('should create a new user', async () => { + await request + .post(api('users.create')) + .set(credentials) + .send({ + email: apiEmail, + name: apiUsername, + username: apiUsername, + password, + active: true, + roles: ['user'], + joinDefaultChannels: true, + verified: true, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('user.username', apiUsername); + expect(res.body).to.have.nested.property('user.emails[0].address', apiEmail); + expect(res.body).to.have.nested.property('user.active', true); + expect(res.body).to.have.nested.property('user.name', apiUsername); + expect(res.body).to.not.have.nested.property('user.e2e'); + + expect(res.body).to.not.have.nested.property('user.customFields'); + + targetUser._id = res.body.user._id; + targetUser.username = res.body.user.username; + }); + }); + + after(async () => { + await deleteUser(targetUser); + }); + it('enabling E2E in server and generating keys to user...', async () => { await updateSetting('E2E_Enable', true); await request @@ -71,145 +97,101 @@ describe('[Users]', function () { }); describe('[/users.create]', () => { - before((done) => clearCustomFields(done)); - after((done) => clearCustomFields(done)); + before(async () => clearCustomFields()); + after(async () => clearCustomFields()); + + it('should create a new user with custom fields', async () => { + await setCustomFields({ customFieldText }); + + const username = `customField_${apiUsername}`; + const email = `customField_${apiEmail}`; + const customFields = { customFieldText: 'success' }; + + let user; - it('should create a new user', async () => { await request .post(api('users.create')) .set(credentials) .send({ - email: apiEmail, - name: apiUsername, - username: apiUsername, + email, + name: username, + username, password, active: true, roles: ['user'], joinDefaultChannels: true, verified: true, + customFields, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('user.username', apiUsername); - expect(res.body).to.have.nested.property('user.emails[0].address', apiEmail); + expect(res.body).to.have.nested.property('user.username', username); + expect(res.body).to.have.nested.property('user.emails[0].address', email); expect(res.body).to.have.nested.property('user.active', true); - expect(res.body).to.have.nested.property('user.name', apiUsername); + expect(res.body).to.have.nested.property('user.name', username); + expect(res.body).to.have.nested.property('user.customFields.customFieldText', 'success'); expect(res.body).to.not.have.nested.property('user.e2e'); - expect(res.body).to.not.have.nested.property('user.customFields'); - - targetUser._id = res.body.user._id; - targetUser.username = res.body.user.username; + user = res.body.user; }); - await request - .post(api('login')) - .send({ - user: apiUsername, - password, - }) - .expect('Content-Type', 'application/json') - .expect(200); + await deleteUser(user); }); - it('should create a new user with custom fields', (done) => { - setCustomFields({ customFieldText }, (error) => { - if (error) { - return done(error); - } - - const username = `customField_${apiUsername}`; - const email = `customField_${apiEmail}`; - const customFields = { customFieldText: 'success' }; - + function failCreateUser(name) { + it(`should not create a new user if username is the reserved word ${name}`, (done) => { request .post(api('users.create')) .set(credentials) .send({ - email, - name: username, - username, + email: `create_user_fail_${apiEmail}`, + name: `create_user_fail_${apiUsername}`, + username: name, password, active: true, roles: ['user'], joinDefaultChannels: true, verified: true, - customFields, }) .expect('Content-Type', 'application/json') - .expect(200) + .expect(400) .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('user.username', username); - expect(res.body).to.have.nested.property('user.emails[0].address', email); - expect(res.body).to.have.nested.property('user.active', true); - expect(res.body).to.have.nested.property('user.name', username); - expect(res.body).to.have.nested.property('user.customFields.customFieldText', 'success'); - expect(res.body).to.not.have.nested.property('user.e2e'); + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', `${name} is blocked and can't be used! [error-blocked-username]`); }) .end(done); }); - }); + } - function failCreateUser(name) { - it(`should not create a new user if username is the reserved word ${name}`, (done) => { - request + function failUserWithCustomField(field) { + it(`should not create a user if a custom field ${field.reason}`, async () => { + await setCustomFields({ customFieldText }); + + const customFields = {}; + customFields[field.name] = field.value; + + await request .post(api('users.create')) .set(credentials) .send({ - email: `create_user_fail_${apiEmail}`, - name: `create_user_fail_${apiUsername}`, - username: name, + email: `customField_fail_${apiEmail}`, + name: `customField_fail_${apiUsername}`, + username: `customField_fail_${apiUsername}`, password, active: true, roles: ['user'], joinDefaultChannels: true, verified: true, + customFields, }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', `${name} is blocked and can't be used! [error-blocked-username]`); - }) - .end(done); - }); - } - - function failUserWithCustomField(field) { - it(`should not create a user if a custom field ${field.reason}`, (done) => { - setCustomFields({ customFieldText }, (error) => { - if (error) { - return done(error); - } - - const customFields = {}; - customFields[field.name] = field.value; - - request - .post(api('users.create')) - .set(credentials) - .send({ - email: `customField_fail_${apiEmail}`, - name: `customField_fail_${apiUsername}`, - username: `customField_fail_${apiUsername}`, - password, - active: true, - roles: ['user'], - joinDefaultChannels: true, - verified: true, - customFields, - }) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'error-user-registration-custom-field'); - }) - .end(done); - }); + expect(res.body).to.have.property('errorType', 'error-user-registration-custom-field'); + }); }); } @@ -226,12 +208,16 @@ describe('[Users]', function () { }); describe('users default roles configuration', () => { + const users = []; + before(async () => { await updateSetting('Accounts_Registration_Users_Default_Roles', 'user,admin'); }); after(async () => { await updateSetting('Accounts_Registration_Users_Default_Roles', 'user'); + + await Promise.all(users.map((user) => deleteUser(user))); }); it('should create a new user with default roles', (done) => { @@ -256,6 +242,8 @@ describe('[Users]', function () { expect(res.body).to.have.nested.property('user.active', true); expect(res.body).to.have.nested.property('user.name', username); expect(res.body.user.roles).to.have.members(['user', 'admin']); + + users.push(res.body.user); }) .end(done); }); @@ -283,6 +271,8 @@ describe('[Users]', function () { expect(res.body).to.have.nested.property('user.active', true); expect(res.body).to.have.nested.property('user.name', username); expect(res.body.user.roles).to.have.members(['guest']); + + users.push(res.body.user); }) .end(done); }); @@ -292,6 +282,10 @@ describe('[Users]', function () { describe('[/users.register]', () => { const email = `email@email${Date.now()}.com`; const username = `myusername${Date.now()}`; + let user; + + after(async () => deleteUser(user)); + it('should register new user', (done) => { request .post(api('users.register')) @@ -308,6 +302,7 @@ describe('[Users]', function () { expect(res.body).to.have.nested.property('user.username', username); expect(res.body).to.have.nested.property('user.active', true); expect(res.body).to.have.nested.property('user.name', 'name'); + user = res.body.user; }) .end(done); }); @@ -331,9 +326,11 @@ describe('[Users]', function () { }); describe('[/users.info]', () => { - after(() => { - updatePermission('view-other-user-channels', ['admin']); - updatePermission('view-full-other-user-info', ['admin']); + after(async () => { + await Promise.all([ + updatePermission('view-other-user-channels', ['admin']), + updatePermission('view-full-other-user-info', ['admin']), + ]); }); it('should return an error when the user does not exist', (done) => { @@ -476,26 +473,30 @@ describe('[Users]', function () { }); it('should correctly route users that have `ufs` in their username', async () => { + const ufsUsername = `ufs-${Date.now()}`; + let user; + await request .post(api('users.create')) .set(credentials) .send({ - email: 'me@email.com', + email: `me-${Date.now()}@email.com`, name: 'testuser', - username: 'ufs', + username: ufsUsername, password: '1234', }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); + user = res.body.user; }); await request .get(api('users.info')) .set(credentials) .query({ - username: 'ufs', + username: ufsUsername, }) .expect('Content-Type', 'application/json') .expect(200) @@ -503,9 +504,11 @@ describe('[Users]', function () { expect(res.body).to.have.property('success', true); expect(res.body.user).to.have.property('type', 'user'); expect(res.body.user).to.have.property('name', 'testuser'); - expect(res.body.user).to.have.property('username', 'ufs'); + expect(res.body.user).to.have.property('username', ufsUsername); expect(res.body.user).to.have.property('active', true); }); + + await deleteUser(user); }); }); describe('[/users.getPresence]', () => { @@ -549,10 +552,10 @@ describe('[Users]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('full', true); - expect(res.body) - .to.have.property('users') - .to.have.property('0') - .to.deep.have.all.keys('_id', 'avatarETag', 'username', 'name', 'status', 'utcOffset'); + + const user = res.body.users.find((user) => user.username === 'rocket.cat'); + + expect(user).to.have.all.keys('_id', 'avatarETag', 'username', 'name', 'status', 'utcOffset'); }) .end(done); }); @@ -583,10 +586,10 @@ describe('[Users]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('full', true); - expect(res.body) - .to.have.property('users') - .to.have.property('0') - .to.deep.have.all.keys('_id', 'avatarETag', 'username', 'name', 'status', 'utcOffset'); + + const user = res.body.users.find((user) => user.username === 'rocket.cat'); + + expect(user).to.have.all.keys('_id', 'avatarETag', 'username', 'name', 'status', 'utcOffset'); }) .end(done); }); @@ -599,68 +602,59 @@ describe('[Users]', function () { let user2; let user2Credentials; - before((done) => { - const createDeactivatedUser = async () => { - const username = `deactivated_${Date.now()}${apiUsername}`; - const email = `deactivated_+${Date.now()}${apiEmail}`; - - const userData = { - email, - name: username, - username, - password, - active: false, - }; - - deactivatedUser = await createUser(userData); - - expect(deactivatedUser).to.not.be.null; - expect(deactivatedUser).to.have.nested.property('username', username); - expect(deactivatedUser).to.have.nested.property('emails[0].address', email); - expect(deactivatedUser).to.have.nested.property('active', false); - expect(deactivatedUser).to.have.nested.property('name', username); - expect(deactivatedUser).to.not.have.nested.property('e2e'); + before(async () => { + const username = `deactivated_${Date.now()}${apiUsername}`; + const email = `deactivated_+${Date.now()}${apiEmail}`; + + const userData = { + email, + name: username, + username, + password, + active: false, }; - createDeactivatedUser().then(done); - }); - before((done) => - setCustomFields({ customFieldText }, async (error) => { - if (error) { - return done(error); - } - - const username = `customField_${Date.now()}${apiUsername}`; - const email = `customField_+${Date.now()}${apiEmail}`; - const customFields = { customFieldText: 'success' }; + deactivatedUser = await createUser(userData); - const userData = { - email, - name: username, - username, - password, - active: true, - roles: ['user'], - joinDefaultChannels: true, - verified: true, - customFields, - }; + expect(deactivatedUser).to.not.be.null; + expect(deactivatedUser).to.have.nested.property('username', username); + expect(deactivatedUser).to.have.nested.property('emails[0].address', email); + expect(deactivatedUser).to.have.nested.property('active', false); + expect(deactivatedUser).to.have.nested.property('name', username); + expect(deactivatedUser).to.not.have.nested.property('e2e'); + }); - user = await createUser(userData); + before(async () => { + await setCustomFields({ customFieldText }); + + const username = `customField_${Date.now()}${apiUsername}`; + const email = `customField_+${Date.now()}${apiEmail}`; + const customFields = { customFieldText: 'success' }; + + const userData = { + email, + name: username, + username, + password, + active: true, + roles: ['user'], + joinDefaultChannels: true, + verified: true, + customFields, + }; - expect(user).to.not.be.null; - expect(user).to.have.nested.property('username', username); - expect(user).to.have.nested.property('emails[0].address', email); - expect(user).to.have.nested.property('active', true); - expect(user).to.have.nested.property('name', username); - expect(user).to.have.nested.property('customFields.customFieldText', 'success'); - expect(user).to.not.have.nested.property('e2e'); + user = await createUser(userData); - return done(); - }), - ); + expect(user).to.not.be.null; + expect(user).to.have.nested.property('username', username); + expect(user).to.have.nested.property('emails[0].address', email); + expect(user).to.have.nested.property('active', true); + expect(user).to.have.nested.property('name', username); + expect(user).to.have.nested.property('customFields.customFieldText', 'success'); + expect(user).to.not.have.nested.property('e2e'); + }); - after((done) => clearCustomFields(done)); + after(async () => clearCustomFields()); before(async () => { user2 = await createUser({ joinDefaultChannels: false }); @@ -668,6 +662,8 @@ describe('[Users]', function () { }); after(async () => { + await deleteUser(deactivatedUser); + await deleteUser(user); await deleteUser(user2); user2 = undefined; @@ -1281,26 +1277,23 @@ describe('[Users]', function () { }); }); - it('should update the user name when the required permission is applied', (done) => { - updatePermission('edit-other-user-info', ['admin']).then(() => { - updateSetting('Accounts_AllowUsernameChange', false).then(() => { - request - .post(api('users.update')) - .set(credentials) - .send({ - userId: targetUser._id, - data: { - username: 'fake.name', - }, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + it('should update the user name when the required permission is applied', async () => { + await Promise.all([updatePermission('edit-other-user-info', ['admin']), updateSetting('Accounts_AllowUsernameChange', false)]); + + await request + .post(api('users.update')) + .set(credentials) + .send({ + userId: targetUser._id, + data: { + username: `fake.name.${Date.now()}`, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); }); - }); }); it('should return an error when trying update user real name and it is not allowed', (done) => { @@ -2297,6 +2290,8 @@ describe('[Users]', function () { .end(done); }); + after(async () => deleteUser(targetUser)); + it('should return an username suggestion', (done) => { request .get(api('users.getUsernameSuggestion')) @@ -2326,7 +2321,7 @@ describe('[Users]', function () { const testUsername = `test-username-123456-${+new Date()}`; let targetUser; let userCredentials; - it('register a new user...', (done) => { + before((done) => { request .post(api('users.register')) .set(credentials) @@ -2343,7 +2338,7 @@ describe('[Users]', function () { }) .end(done); }); - it('Login...', (done) => { + before((done) => { request .post(api('login')) .send({ @@ -2360,6 +2355,8 @@ describe('[Users]', function () { .end(done); }); + after(async () => deleteUser(targetUser)); + it('should return true if the username is the same user username set', (done) => { request .get(api('users.checkUsernameAvailability')) @@ -2410,7 +2407,7 @@ describe('[Users]', function () { const testUsername = `testuser${+new Date()}`; let targetUser; let userCredentials; - it('register a new user...', (done) => { + before((done) => { request .post(api('users.register')) .set(credentials) @@ -2427,7 +2424,7 @@ describe('[Users]', function () { }) .end(done); }); - it('Login...', (done) => { + before((done) => { request .post(api('login')) .send({ @@ -2444,6 +2441,8 @@ describe('[Users]', function () { .end(done); }); + after(async () => deleteUser(targetUser)); + it('Enable "Accounts_AllowDeleteOwnAccount" setting...', (done) => { request .post('/api/v1/settings/Accounts_AllowDeleteOwnAccount') @@ -2472,23 +2471,22 @@ describe('[Users]', function () { .end(done); }); - it('should delete user own account when the SHA256 hash is in upper case', (done) => { - createUser().then((user) => { - login(user.username, password).then((createdUserCredentials) => { - request - .post(api('users.deleteOwnAccount')) - .set(createdUserCredentials) - .send({ - password: crypto.createHash('sha256').update(password, 'utf8').digest('hex').toUpperCase(), - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + it('should delete user own account when the SHA256 hash is in upper case', async () => { + const user = await createUser(); + const createdUserCredentials = await login(user.username, password); + await request + .post(api('users.deleteOwnAccount')) + .set(createdUserCredentials) + .send({ + password: crypto.createHash('sha256').update(password, 'utf8').digest('hex').toUpperCase(), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); }); - }); + + await deleteUser(user); }); it('should return an error when trying to delete user own account if user is the last room owner', async () => { @@ -2542,6 +2540,9 @@ describe('[Users]', function () { expect(res.body).to.have.property('error', '[user-last-owner]'); expect(res.body).to.have.property('errorType', 'user-last-owner'); }); + + await deleteRoom({ type: 'c', roomId: room._id }); + await deleteUser(user); }); it('should delete user own account if the user is the last room owner and `confirmRelinquish` is set to `true`', async () => { @@ -2594,6 +2595,8 @@ describe('[Users]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); }); + await deleteRoom({ type: 'c', roomId: room._id }); + await deleteUser(user); }); it('should assign a new owner to the room if the last room owner is deleted', async () => { @@ -2661,6 +2664,8 @@ describe('[Users]', function () { expect(res.body.roles[0].roles).to.eql(['owner']); expect(res.body.roles[0].u).to.have.property('_id', credentials['X-User-Id']); }); + await deleteRoom({ type: 'c', roomId: room._id }); + await deleteUser(user); }); }); @@ -2763,6 +2768,8 @@ describe('[Users]', function () { expect(res.body).to.have.property('error', '[user-last-owner]'); expect(res.body).to.have.property('errorType', 'user-last-owner'); }); + + await deleteRoom({ type: 'c', roomId: room._id }); }); it('should delete user account if the user is the last room owner and `confirmRelinquish` is set to `true`', async () => { @@ -2813,6 +2820,8 @@ describe('[Users]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); }); + + await deleteRoom({ type: 'c', roomId: room._id }); }); it('should delete user account when logged user has "delete-user" permission', async () => { @@ -2893,6 +2902,8 @@ describe('[Users]', function () { expect(res.body.roles[0].roles).to.eql(['owner']); expect(res.body.roles[0].u).to.have.property('_id', credentials['X-User-Id']); }); + + await deleteRoom({ type: 'c', roomId: room._id }); }); }); @@ -3241,6 +3252,8 @@ describe('[Users]', function () { expect(res.body).to.have.property('error', '[user-last-owner]'); expect(res.body).to.have.property('errorType', 'user-last-owner'); }); + + await deleteRoom({ type: 'c', roomId: room._id }); }); it('should set other user status to inactive if the user is the last owner of a room and `confirmRelinquish` is set to `true`', async () => { @@ -3305,6 +3318,8 @@ describe('[Users]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); }); + + await deleteRoom({ type: 'c', roomId: room._id }); }); it('should set other user as room owner if the last owner of a room is deactivated and `confirmRelinquish` is set to `true`', async () => { @@ -3396,6 +3411,8 @@ describe('[Users]', function () { expect(res.body.roles[1].roles).to.eql(['owner']); expect(res.body.roles[1].u).to.have.property('_id', credentials['X-User-Id']); }); + + await deleteRoom({ type: 'c', roomId: room._id }); }); it('should return an error when trying to set other user active status and has not the necessary permission(edit-other-user-active-status)', (done) => { @@ -3464,6 +3481,8 @@ describe('[Users]', function () { expect(user).to.have.property('roles'); expect(user.roles).to.be.an('array').of.length(2); expect(user.roles).to.include('user', 'livechat-agent'); + + await deleteUser(testUser); }); }); @@ -3516,6 +3535,10 @@ describe('[Users]', function () { .end(done); }); + after(async () => { + await deleteUser(testUser); + }); + it('should fail to deactivate if user doesnt have edit-other-user-active-status permission', (done) => { updatePermission('edit-other-user-active-status', []).then(() => { request @@ -3563,7 +3586,7 @@ describe('[Users]', function () { .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('count', 2); + expect(res.body).to.have.property('count', 1); }) .end(done); }); @@ -3690,7 +3713,7 @@ describe('[Users]', function () { updatePermission('view-outside-room', ['admin', 'owner', 'moderator', 'user']); }); - describe('[without permission]', () => { + describe('[without permission]', function () { let user; let userCredentials; let user2; @@ -3711,6 +3734,12 @@ describe('[Users]', function () { roomId = await createChannel(userCredentials, `channel.autocomplete.${Date.now()}`); }); + after(async () => { + await deleteRoom({ type: 'c', roomId }); + await deleteUser(user); + await deleteUser(user2); + }); + it('should return an empty list when the user does not have any subscription', (done) => { request .get(api('users.autocomplete?selector={}')) @@ -4126,6 +4155,10 @@ describe('[Users]', function () { .then(() => done()); }); + after(async () => { + await deleteUser(testUser); + }); + it('should list both channels', (done) => { request .get(api('users.listTeams')) @@ -4151,14 +4184,19 @@ describe('[Users]', function () { describe('[/users.logout]', () => { let user; let otherUser; + let userCredentials; + before(async () => { user = await createUser(); otherUser = await createUser(); }); + before(async () => { + userCredentials = await login(user.username, password); + }); + after(async () => { await deleteUser(user); await deleteUser(otherUser); - user = undefined; }); it('should throw unauthorized error to user w/o "logout-other-user" permission', (done) => { @@ -4187,7 +4225,7 @@ describe('[Users]', function () { it('should logout the requester', (done) => { updatePermission('logout-other-user', []).then(() => { - request.post(api('users.logout')).set(credentials).expect('Content-Type', 'application/json').expect(200).end(done); + request.post(api('users.logout')).set(userCredentials).expect('Content-Type', 'application/json').expect(200).end(done); }); }); }); diff --git a/apps/meteor/tests/end-to-end/api/10-subscriptions.js b/apps/meteor/tests/end-to-end/api/10-subscriptions.js index f547895eb8a4..531291a99216 100644 --- a/apps/meteor/tests/end-to-end/api/10-subscriptions.js +++ b/apps/meteor/tests/end-to-end/api/10-subscriptions.js @@ -236,7 +236,8 @@ describe('[Subscriptions]', function () { before(async () => { user = await createUser({ username: 'testthread123', password: 'testthread123' }); threadUserCredentials = await login('testthread123', 'testthread123'); - request + + const res = await request .post(api('chat.sendMessage')) .set(threadUserCredentials) .send({ @@ -244,14 +245,13 @@ describe('[Subscriptions]', function () { rid: testChannel._id, msg: 'Starting a Thread', }, - }) - .end((_, res) => { - threadId = res.body.message._id; }); + + threadId = res.body.message._id; }); - after((done) => { - deleteUser(user).then(done); + after(async () => { + await deleteUser(user); }); it('should mark threads as read', async () => { From 93a0859e87f0a115152e79e224fb42c10748c5fe Mon Sep 17 00:00:00 2001 From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Fri, 20 Oct 2023 02:23:53 +0530 Subject: [PATCH 33/38] fix: Unnecessary username validation on account profile form (#30677) --- .changeset/empty-files-know.md | 5 +++++ .../client/views/account/profile/AccountProfileForm.tsx | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/empty-files-know.md diff --git a/.changeset/empty-files-know.md b/.changeset/empty-files-know.md new file mode 100644 index 000000000000..5e6fb8f751b2 --- /dev/null +++ b/.changeset/empty-files-know.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fix unnecessary username validation on accounts profile form diff --git a/apps/meteor/client/views/account/profile/AccountProfileForm.tsx b/apps/meteor/client/views/account/profile/AccountProfileForm.tsx index ed97b95caae8..65b3a0967d49 100644 --- a/apps/meteor/client/views/account/profile/AccountProfileForm.tsx +++ b/apps/meteor/client/views/account/profile/AccountProfileForm.tsx @@ -66,6 +66,7 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle const { email, avatar, username } = watch(); const previousEmail = user ? getUserEmailAddress(user) : ''; + const previousUsername = user?.username || ''; const isUserVerified = user?.emails?.[0]?.verified ?? false; const mutateConfirmationEmail = useMutation({ @@ -87,6 +88,10 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle return; } + if (username === previousUsername) { + return; + } + if (!namesRegex.test(username)) { return t('error-invalid-username'); } From b85df55030f9aaf351d15fe66ee0ec008bfc9691 Mon Sep 17 00:00:00 2001 From: csuadev <72958726+csuadev@users.noreply.github.com> Date: Thu, 19 Oct 2023 16:46:06 -0500 Subject: [PATCH 34/38] fix: UI issue on marketplace filters (#30660) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Júlia Jaeger Foresti <60678893+juliajforesti@users.noreply.github.com> --- .changeset/cyan-mangos-do.md | 5 +++ .../CategoryFilter/CategoryDropDownAnchor.tsx | 41 ++++++++++++------- .../RadioDropDown/RadioDownAnchor.tsx | 22 ++++++---- 3 files changed, 44 insertions(+), 24 deletions(-) create mode 100644 .changeset/cyan-mangos-do.md diff --git a/.changeset/cyan-mangos-do.md b/.changeset/cyan-mangos-do.md new file mode 100644 index 000000000000..e188686c82d5 --- /dev/null +++ b/.changeset/cyan-mangos-do.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: UI issue on marketplace filters diff --git a/apps/meteor/client/views/marketplace/components/CategoryFilter/CategoryDropDownAnchor.tsx b/apps/meteor/client/views/marketplace/components/CategoryFilter/CategoryDropDownAnchor.tsx index 91e66683e66f..b3e43fda942f 100644 --- a/apps/meteor/client/views/marketplace/components/CategoryFilter/CategoryDropDownAnchor.tsx +++ b/apps/meteor/client/views/marketplace/components/CategoryFilter/CategoryDropDownAnchor.tsx @@ -1,4 +1,6 @@ -import { Box, Button, Icon } from '@rocket.chat/fuselage'; +import type { Button } from '@rocket.chat/fuselage'; +import { Box, Icon } from '@rocket.chat/fuselage'; +import colorTokens from '@rocket.chat/fuselage-tokens/colors.json'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps, MouseEventHandler } from 'react'; import React, { forwardRef } from 'react'; @@ -15,33 +17,42 @@ const CategoryDropDownAnchor = forwardRef {selectedCategoriesCount > 0 && ( {selectedCategoriesCount} diff --git a/apps/meteor/client/views/marketplace/components/RadioDropDown/RadioDownAnchor.tsx b/apps/meteor/client/views/marketplace/components/RadioDropDown/RadioDownAnchor.tsx index 36e4ff55657f..f480b2a60280 100644 --- a/apps/meteor/client/views/marketplace/components/RadioDropDown/RadioDownAnchor.tsx +++ b/apps/meteor/client/views/marketplace/components/RadioDropDown/RadioDownAnchor.tsx @@ -1,4 +1,5 @@ -import { Box, Button, Icon } from '@rocket.chat/fuselage'; +import type { Button } from '@rocket.chat/fuselage'; +import { Box, Icon } from '@rocket.chat/fuselage'; import type { ComponentProps } from 'react'; import React, { forwardRef } from 'react'; @@ -14,22 +15,25 @@ const RadioDownAnchor = forwardRef(functi return ( {selected} From b9a3381d9394d39bb22629c9fc951c2407e00db8 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 19 Oct 2023 16:47:32 -0600 Subject: [PATCH 35/38] test: `ShouldPreventAction` (#30690) --- ee/packages/license/src/license.spec.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/ee/packages/license/src/license.spec.ts b/ee/packages/license/src/license.spec.ts index 989be7b69ae1..b637ee33ddfd 100644 --- a/ee/packages/license/src/license.spec.ts +++ b/ee/packages/license/src/license.spec.ts @@ -41,6 +41,30 @@ it('should prevent if the counter is equal or over the limit', async () => { await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true); }); +it.skip('should not prevent an action if another limit is over the limit', async () => { + const licenseManager = await getReadyLicenseManager(); + + const license = await new MockedLicenseBuilder() + .withLimits('activeUsers', [ + { + max: 10, + behavior: 'prevent_action', + }, + ]) + .withLimits('monthlyActiveContacts', [ + { + max: 10, + behavior: 'prevent_action', + }, + ]); + + await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true); + + licenseManager.setLicenseLimitCounter('activeUsers', () => 11); + licenseManager.setLicenseLimitCounter('monthlyActiveContacts', () => 2); + await expect(licenseManager.shouldPreventAction('monthlyActiveContacts')).resolves.toBe(false); +}); + describe('Validate License Limits', () => { describe('prevent_action behavior', () => { describe('during the licensing apply', () => { From 53cf1f5940c76e6d4df132e3ca8e7118206d1ea5 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 19 Oct 2023 19:25:00 -0600 Subject: [PATCH 36/38] refactor: Move functions out of `livechat.js` (#30664) --- .../app/apps/server/bridges/livechat.ts | 2 +- .../app/livechat/imports/server/rest/sms.js | 2 +- .../app/livechat/server/api/v1/message.ts | 16 +- .../app/livechat/server/api/v1/visitor.ts | 2 +- apps/meteor/app/livechat/server/lib/Helper.ts | 6 +- .../app/livechat/server/lib/Livechat.js | 191 +--------------- .../app/livechat/server/lib/LivechatTyped.ts | 207 +++++++++++++++++- .../server/methods/returnAsInquiry.ts | 4 +- .../server/methods/sendMessageLivechat.ts | 28 +-- apps/meteor/app/livechat/server/startup.ts | 10 +- .../server/lib/AutoTransferChatScheduler.ts | 4 +- .../EmailInbox/EmailInbox_Incoming.ts | 3 +- 12 files changed, 238 insertions(+), 237 deletions(-) diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 5b6c76257667..70802f280095 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -44,7 +44,7 @@ export class AppLivechatBridge extends LivechatBridge { throw new Error('Invalid token for livechat message'); } - const msg = await Livechat.sendMessage({ + const msg = await LivechatTyped.sendMessage({ guest: this.orch.getConverters()?.get('visitors').convertAppVisitor(message.visitor), message: await this.orch.getConverters()?.get('messages').convertAppMessage(message), agent: undefined, diff --git a/apps/meteor/app/livechat/imports/server/rest/sms.js b/apps/meteor/app/livechat/imports/server/rest/sms.js index 7ecb3b3fc100..9d2bee133784 100644 --- a/apps/meteor/app/livechat/imports/server/rest/sms.js +++ b/apps/meteor/app/livechat/imports/server/rest/sms.js @@ -182,7 +182,7 @@ API.v1.addRoute('livechat/sms-incoming/:service', { }; try { - const msg = SMSService.response.call(this, await Livechat.sendMessage(sendMessage)); + const msg = SMSService.response.call(this, await LivechatTyped.sendMessage(sendMessage)); setImmediate(async () => { if (sms.extra) { if (sms.extra.fromCountry) { diff --git a/apps/meteor/app/livechat/server/api/v1/message.ts b/apps/meteor/app/livechat/server/api/v1/message.ts index 0d5a22b90d89..1dcf54e403a6 100644 --- a/apps/meteor/app/livechat/server/api/v1/message.ts +++ b/apps/meteor/app/livechat/server/api/v1/message.ts @@ -17,7 +17,6 @@ import { isWidget } from '../../../../api/server/helpers/isWidget'; import { loadMessageHistory } from '../../../../lib/server/functions/loadMessageHistory'; import { settings } from '../../../../settings/server'; import { normalizeMessageFileUpload } from '../../../../utils/server/functions/normalizeMessageFileUpload'; -import { Livechat } from '../../lib/Livechat'; import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; import { findGuest, findRoom, normalizeHttpHeaderData } from '../lib/livechat'; @@ -67,7 +66,7 @@ API.v1.addRoute( }, }; - const result = await Livechat.sendMessage(sendMessage); + const result = await LivechatTyped.sendMessage(sendMessage); if (result) { const message = await Messages.findOneById(_id); if (!message) { @@ -176,7 +175,7 @@ API.v1.addRoute( throw new Error('invalid-message'); } - const result = await Livechat.deleteMessage({ guest, message }); + const result = await LivechatTyped.deleteMessage({ guest, message }); if (result) { return API.v1.success({ message: { @@ -272,10 +271,15 @@ API.v1.addRoute( visitor = await LivechatVisitors.findOneEnabledById(visitorId); } + const guest = visitor; + if (!guest) { + throw new Error('error-invalid-token'); + } + const sentMessages = await Promise.all( this.bodyParams.messages.map(async (message: { msg: string }): Promise<{ username: string; msg: string; ts: number }> => { const sendMessage = { - guest: visitor, + guest, message: { _id: Random.id(), rid, @@ -288,8 +292,8 @@ API.v1.addRoute( }, }, }; - // @ts-expect-error -- Typings on sendMessage are wrong - const sentMessage = await Livechat.sendMessage(sendMessage); + + const sentMessage = await LivechatTyped.sendMessage(sendMessage); return { username: sentMessage.u.username, msg: sentMessage.msg, diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index 84f7b96e155d..6488d34eab7a 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -121,7 +121,7 @@ API.v1.addRoute('livechat/visitor/:token', { } const { _id } = visitor; - const result = await Livechat.removeGuest(_id); + const result = await LivechatTyped.removeGuest(_id); if (!result.modifiedCount) { throw new Meteor.Error('error-removing-visitor', 'An error ocurred while deleting visitor'); } diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index 63cbbd6998ef..4acbdf5090ad 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -437,7 +437,7 @@ export const forwardRoomToAgent = async (room: IOmnichannelRoom, transferData: T return false; } - await Livechat.saveTransferHistory(room, transferData); + await LivechatTyped.saveTransferHistory(room, transferData); const { servedBy } = roomTaken; if (servedBy) { @@ -537,7 +537,7 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi logger.debug( `Routing algorithm doesn't support auto assignment (using ${RoutingManager.methodName}). Chat will be on department queue`, ); - await Livechat.saveTransferHistory(room, transferData); + await LivechatTyped.saveTransferHistory(room, transferData); return RoutingManager.unassignAgent(inquiry, departmentId); } @@ -573,7 +573,7 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi } } - await Livechat.saveTransferHistory(room, transferData); + await LivechatTyped.saveTransferHistory(room, transferData); if (oldServedBy) { // if chat is queued then we don't ignore the new servedBy agent bcs at this // point the chat is not assigned to him/her and it is still in the queue diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 837a8eb7309b..b208c9fb5e85 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -8,11 +8,9 @@ import { LivechatRooms, LivechatInquiry, Subscriptions, - Messages, LivechatDepartment as LivechatDepartmentRaw, Rooms, Users, - ReadReceipts, } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -25,15 +23,11 @@ import { i18n } from '../../../../server/lib/i18n'; import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; import { removeUserFromRolesAsync } from '../../../../server/lib/roles/removeUserFromRoles'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { FileUpload } from '../../../file-upload/server'; -import { deleteMessage } from '../../../lib/server/functions/deleteMessage'; -import { sendMessage } from '../../../lib/server/functions/sendMessage'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; import { businessHourManager } from '../business-hour'; import { Analytics } from './Analytics'; -import { normalizeTransferredByData, parseAgentCustomFields, updateDepartmentAgents } from './Helper'; -import { Livechat as LivechatTyped } from './LivechatTyped'; +import { parseAgentCustomFields, updateDepartmentAgents } from './Helper'; import { RoutingManager } from './RoutingManager'; const logger = new Logger('Livechat'); @@ -43,41 +37,6 @@ export const Livechat = { logger, - async sendMessage({ guest, message, roomInfo, agent }) { - const { room, newRoom } = await LivechatTyped.getRoom(guest, message, roomInfo, agent); - if (guest.name) { - message.alias = guest.name; - } - return Object.assign(await sendMessage(guest, message, room), { - newRoom, - showConnecting: this.showConnecting(), - }); - }, - - async deleteMessage({ guest, message }) { - Livechat.logger.debug(`Attempting to delete a message by visitor ${guest._id}`); - check(message, Match.ObjectIncluding({ _id: String })); - - const msg = await Messages.findOneById(message._id); - if (!msg || !msg._id) { - return; - } - - const deleteAllowed = settings.get('Message_AllowDeleting'); - const editOwn = msg.u && msg.u._id === guest._id; - - if (!deleteAllowed || !editOwn) { - Livechat.logger.debug('Cannot delete message: not allowed'); - throw new Meteor.Error('error-action-not-allowed', 'Message deleting not allowed', { - method: 'livechatDeleteMessage', - }); - } - - await deleteMessage(message, guest); - - return true; - }, - async saveGuest(guestData, userId) { const { _id, name, email, phone, livechatData = {} } = guestData; Livechat.logger.debug(`Saving data for visitor ${_id}`); @@ -234,111 +193,6 @@ export const Livechat = { return Message.saveSystemMessage('livechat_navigation_history', roomId, `${pageTitle} - ${pageUrl}`, user, extraData); }, - async saveTransferHistory(room, transferData) { - Livechat.logger.debug(`Saving transfer history for room ${room._id}`); - const { departmentId: previousDepartment } = room; - const { department: nextDepartment, transferredBy, transferredTo, scope, comment } = transferData; - - check( - transferredBy, - Match.ObjectIncluding({ - _id: String, - username: String, - name: Match.Maybe(String), - type: String, - }), - ); - - const { _id, username } = transferredBy; - const scopeData = scope || (nextDepartment ? 'department' : 'agent'); - Livechat.logger.debug(`Storing new chat transfer of ${room._id} [Transfered by: ${_id} to ${scopeData}]`); - - const transfer = { - transferData: { - transferredBy, - ts: new Date(), - scope: scopeData, - comment, - ...(previousDepartment && { previousDepartment }), - ...(nextDepartment && { nextDepartment }), - ...(transferredTo && { transferredTo }), - }, - }; - - const type = 'livechat_transfer_history'; - const transferMessage = { - t: type, - rid: room._id, - ts: new Date(), - msg: '', - u: { - _id, - username, - }, - groupable: false, - }; - - Object.assign(transferMessage, transfer); - - await sendMessage(transferredBy, transferMessage, room); - }, - - async returnRoomAsInquiry(rid, departmentId, overrideTransferData = {}) { - Livechat.logger.debug(`Transfering room ${rid} to ${departmentId ? 'department' : ''} queue`); - const room = await LivechatRooms.findOneById(rid); - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { - method: 'livechat:returnRoomAsInquiry', - }); - } - - if (!room.open) { - throw new Meteor.Error('room-closed', 'Room closed', { - method: 'livechat:returnRoomAsInquiry', - }); - } - - if (room.onHold) { - throw new Meteor.Error('error-room-onHold', 'Room On Hold', { - method: 'livechat:returnRoomAsInquiry', - }); - } - - if (!room.servedBy) { - return false; - } - - const user = await Users.findOneById(room.servedBy._id); - if (!user || !user._id) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'livechat:returnRoomAsInquiry', - }); - } - - // find inquiry corresponding to room - const inquiry = await LivechatInquiry.findOne({ rid }); - if (!inquiry) { - return false; - } - - const transferredBy = normalizeTransferredByData(user, room); - Livechat.logger.debug(`Transfering room ${room._id} by user ${transferredBy._id}`); - const transferData = { roomId: rid, scope: 'queue', departmentId, transferredBy, ...overrideTransferData }; - try { - await this.saveTransferHistory(room, transferData); - await RoutingManager.unassignAgent(inquiry, departmentId); - } catch (e) { - this.logger.error(e); - throw new Meteor.Error('error-returning-inquiry', 'Error returning inquiry to the queue', { - method: 'livechat:returnRoomAsInquiry', - }); - } - - callbacks.runAsync('livechat:afterReturnRoomAsInquiry', { room }); - - return true; - }, - async getLivechatRoomGuestInfo(room) { const visitor = await LivechatVisitors.findOneEnabledById(room.v._id); const agent = await Users.findOneById(room.servedBy && room.servedBy._id); @@ -481,55 +335,12 @@ export const Livechat = { return removeUserFromRolesAsync(user._id, ['livechat-manager']); }, - async removeGuest(_id) { - const guest = await LivechatVisitors.findOneEnabledById(_id, { projection: { _id: 1, token: 1 } }); - if (!guest) { - throw new Meteor.Error('error-invalid-guest', 'Invalid guest', { - method: 'livechat:removeGuest', - }); - } - - await this.cleanGuestHistory(guest); - return LivechatVisitors.disableById(_id); - }, - async setUserStatusLivechat(userId, status) { const user = await Users.setLivechatStatus(userId, status); callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status }); return user; }, - async setUserStatusLivechatIf(userId, status, condition, fields) { - const user = await Users.setLivechatStatusIf(userId, status, condition, fields); - callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status }); - return user; - }, - - async cleanGuestHistory(guest) { - const { token } = guest; - - // This shouldn't be possible, but just in case - if (!token) { - throw new Error('error-invalid-guest'); - } - - const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); - const cursor = LivechatRooms.findByVisitorToken(token, extraQuery); - for await (const room of cursor) { - await Promise.all([ - FileUpload.removeFilesByRoomId(room._id), - Messages.removeByRoomId(room._id), - ReadReceipts.removeByRoomId(room._id), - ]); - } - - await Promise.all([ - Subscriptions.removeByVisitorToken(token), - LivechatRooms.removeByVisitorToken(token), - LivechatInquiry.removeByVisitorToken(token), - ]); - }, - async saveDepartmentAgents(_id, departmentAgents) { check(_id, String); check(departmentAgents, { diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 293b15e8d63c..32cb5c83acd9 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -15,6 +15,9 @@ import type { ILivechatDepartment, AtLeast, TransferData, + MessageAttachment, + IMessageInbox, + ILivechatAgentStatus, } from '@rocket.chat/core-typings'; import { UserStatus, isOmnichannelRoom } from '@rocket.chat/core-typings'; import { Logger, type MainLogger } from '@rocket.chat/logger'; @@ -34,13 +37,15 @@ import { import { Random } from '@rocket.chat/random'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import moment from 'moment-timezone'; -import type { FindCursor, UpdateFilter } from 'mongodb'; +import type { Filter, FindCursor, UpdateFilter } from 'mongodb'; import { Apps, AppEvents } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; import { canAccessRoomAsync } from '../../../authorization/server'; import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; +import { FileUpload } from '../../../file-upload/server'; +import { deleteMessage } from '../../../lib/server/functions/deleteMessage'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; import { updateMessage } from '../../../lib/server/functions/updateMessage'; import * as Mailer from '../../../mailer/server/api'; @@ -89,6 +94,40 @@ type OfflineMessageData = { host?: string; }; +export interface ILivechatMessage { + token: string; + _id: string; + rid: string; + msg: string; + file?: { + _id: string; + name?: string; + type?: string; + size?: number; + description?: string; + identify?: { size: { width: number; height: number } }; + format?: string; + }; + files?: { + _id: string; + name?: string; + type?: string; + size?: number; + description?: string; + identify?: { size: { width: number; height: number } }; + format?: string; + }[]; + attachments?: MessageAttachment[]; + alias?: string; + groupable?: boolean; + blocks?: IMessage['blocks']; + email?: IMessageInbox['email']; +} + +type AKeyOf = { + [K in keyof T]?: T[K]; +}; + const dnsResolveMx = util.promisify(dns.resolveMx); class LivechatClass { @@ -1123,6 +1162,172 @@ class LivechatClass { void callbacks.run('livechat.offlineMessage', data); }); } + + async sendMessage({ + guest, + message, + roomInfo, + agent, + }: { + guest: ILivechatVisitor; + message: ILivechatMessage; + roomInfo: { + source?: IOmnichannelRoom['source']; + [key: string]: unknown; + }; + agent?: SelectedAgent; + }) { + const { room, newRoom } = await this.getRoom(guest, message, roomInfo, agent); + if (guest.name) { + message.alias = guest.name; + } + return Object.assign(await sendMessage(guest, message, room), { + newRoom, + showConnecting: this.showConnecting(), + }); + } + + async removeGuest(_id: string) { + const guest = await LivechatVisitors.findOneEnabledById(_id, { projection: { _id: 1, token: 1 } }); + if (!guest) { + throw new Error('error-invalid-guest'); + } + + await this.cleanGuestHistory(guest); + return LivechatVisitors.disableById(_id); + } + + async cleanGuestHistory(guest: ILivechatVisitor) { + const { token } = guest; + + // This shouldn't be possible, but just in case + if (!token) { + throw new Error('error-invalid-guest'); + } + + const cursor = LivechatRooms.findByVisitorToken(token); + for await (const room of cursor) { + await Promise.all([ + FileUpload.removeFilesByRoomId(room._id), + Messages.removeByRoomId(room._id), + ReadReceipts.removeByRoomId(room._id), + ]); + } + + await Promise.all([ + Subscriptions.removeByVisitorToken(token), + LivechatRooms.removeByVisitorToken(token), + LivechatInquiry.removeByVisitorToken(token), + ]); + } + + async deleteMessage({ guest, message }: { guest: ILivechatVisitor; message: IMessage }) { + const deleteAllowed = settings.get('Message_AllowDeleting'); + const editOwn = message.u && message.u._id === guest._id; + + if (!deleteAllowed || !editOwn) { + throw new Error('error-action-not-allowed'); + } + + await deleteMessage(message, guest as unknown as IUser); + + return true; + } + + async setUserStatusLivechatIf(userId: string, status: ILivechatAgentStatus, condition?: Filter, fields?: AKeyOf) { + const user = await Users.setLivechatStatusIf(userId, status, condition, fields); + callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status }); + return user; + } + + async returnRoomAsInquiry(room: IOmnichannelRoom, departmentId?: string, overrideTransferData: any = {}) { + this.logger.debug({ msg: `Transfering room to ${departmentId ? 'department' : ''} queue`, room }); + if (!room.open) { + throw new Meteor.Error('room-closed'); + } + + if (room.onHold) { + throw new Meteor.Error('error-room-onHold'); + } + + if (!room.servedBy) { + return false; + } + + const user = await Users.findOneById(room.servedBy._id); + if (!user?._id) { + throw new Meteor.Error('error-invalid-user'); + } + + // find inquiry corresponding to room + const inquiry = await LivechatInquiry.findOne({ rid: room._id }); + if (!inquiry) { + return false; + } + + const transferredBy = normalizeTransferredByData(user, room); + this.logger.debug(`Transfering room ${room._id} by user ${transferredBy._id}`); + const transferData = { roomId: room._id, scope: 'queue', departmentId, transferredBy, ...overrideTransferData }; + try { + await this.saveTransferHistory(room, transferData); + await RoutingManager.unassignAgent(inquiry, departmentId); + } catch (e) { + this.logger.error(e); + throw new Meteor.Error('error-returning-inquiry'); + } + + callbacks.runAsync('livechat:afterReturnRoomAsInquiry', { room }); + + return true; + } + + async saveTransferHistory(room: IOmnichannelRoom, transferData: TransferData) { + const { departmentId: previousDepartment } = room; + const { department: nextDepartment, transferredBy, transferredTo, scope, comment } = transferData; + + check( + transferredBy, + Match.ObjectIncluding({ + _id: String, + username: String, + name: Match.Maybe(String), + type: String, + }), + ); + + const { _id, username } = transferredBy; + const scopeData = scope || (nextDepartment ? 'department' : 'agent'); + this.logger.info(`Storing new chat transfer of ${room._id} [Transfered by: ${_id} to ${scopeData}]`); + + const transfer = { + transferData: { + transferredBy, + ts: new Date(), + scope: scopeData, + comment, + ...(previousDepartment && { previousDepartment }), + ...(nextDepartment && { nextDepartment }), + ...(transferredTo && { transferredTo }), + }, + }; + + const type = 'livechat_transfer_history'; + const transferMessage = { + t: type, + rid: room._id, + ts: new Date(), + msg: '', + u: { + _id, + username, + }, + groupable: false, + }; + + Object.assign(transferMessage, transfer); + + await sendMessage(transferredBy, transferMessage, room); + } } export const Livechat = new LivechatClass(); diff --git a/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts b/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts index 57a2b0afa3d5..0c12d0df5275 100644 --- a/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts +++ b/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts @@ -4,7 +4,7 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { Livechat } from '../lib/Livechat'; +import { Livechat } from '../lib/LivechatTyped'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -33,6 +33,6 @@ Meteor.methods({ throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:returnAsInquiry' }); } - return Livechat.returnRoomAsInquiry(rid, departmentId); + return Livechat.returnRoomAsInquiry(room, departmentId); }, }); diff --git a/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts b/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts index c7d412ea4a06..516a9bc5081f 100644 --- a/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts +++ b/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts @@ -1,36 +1,12 @@ import { OmnichannelSourceType } from '@rocket.chat/core-typings'; -import type { MessageAttachment } from '@rocket.chat/core-typings'; import { LivechatVisitors } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { settings } from '../../../settings/server'; -import { Livechat } from '../lib/Livechat'; - -interface ILivechatMessage { - token: string; - _id: string; - rid: string; - msg: string; - file?: { - _id: string; - name?: string; - type?: string; - size?: number; - description?: string; - identify?: { size: { width: number; height: number } }; - }; - files?: { - _id: string; - name?: string; - type?: string; - size?: number; - description?: string; - identify?: { size: { width: number; height: number } }; - }[]; - attachments?: MessageAttachment[]; -} +import { Livechat } from '../lib/LivechatTyped'; +import type { ILivechatMessage } from '../lib/LivechatTyped'; interface ILivechatMessageAgent { agentId: string; diff --git a/apps/meteor/app/livechat/server/startup.ts b/apps/meteor/app/livechat/server/startup.ts index f9fce509e39a..c8487f742b3a 100644 --- a/apps/meteor/app/livechat/server/startup.ts +++ b/apps/meteor/app/livechat/server/startup.ts @@ -1,5 +1,5 @@ import type { IUser } from '@rocket.chat/core-typings'; -import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { ILivechatAgentStatus, isOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms } from '@rocket.chat/models'; import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; @@ -13,6 +13,7 @@ import { settings } from '../../settings/server'; import { businessHourManager } from './business-hour'; import { createDefaultBusinessHourIfNotExists } from './business-hour/Helper'; import { Livechat } from './lib/Livechat'; +import { Livechat as LivechatTyped } from './lib/LivechatTyped'; import { RoutingManager } from './lib/RoutingManager'; import { LivechatAgentActivityMonitor } from './statistics/LivechatAgentActivityMonitor'; import './roomAccessValidator.internalService'; @@ -79,6 +80,11 @@ Meteor.startup(async () => { ({ user }: { user: IUser }) => user?.roles?.includes('livechat-agent') && !user?.roles?.includes('bot') && - void Livechat.setUserStatusLivechatIf(user._id, 'not-available', {}, { livechatStatusSystemModified: true }).catch(), + void LivechatTyped.setUserStatusLivechatIf( + user._id, + ILivechatAgentStatus.NOT_AVAILABLE, + {}, + { livechatStatusSystemModified: true }, + ).catch(), ); }); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts index 9d4590836ac9..68044a550277 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts @@ -5,8 +5,8 @@ import { LivechatRooms, Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { MongoInternals } from 'meteor/mongo'; -import { Livechat } from '../../../../../app/livechat/server'; import { forwardRoomToAgent } from '../../../../../app/livechat/server/lib/Helper'; +import { Livechat as LivechatTyped } from '../../../../../app/livechat/server/lib/LivechatTyped'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; import { settings } from '../../../../../app/settings/server'; import { schedulerLogger } from './logger'; @@ -90,7 +90,7 @@ class AutoTransferChatSchedulerClass { if (!RoutingManager.getConfig()?.autoAssignAgent) { this.logger.debug(`Auto-assign agent is disabled, returning room ${roomId} as inquiry`); - await Livechat.returnRoomAsInquiry(room._id, departmentId, { + await LivechatTyped.returnRoomAsInquiry(room, departmentId, { scope: 'autoTransferUnansweredChatsToQueue', comment: timeoutDuration, transferredBy: await this.getSchedulerUser(), diff --git a/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts b/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts index 939d91661650..ebdd9cdcac01 100644 --- a/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts +++ b/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts @@ -12,7 +12,6 @@ import type { ParsedMail, Attachment } from 'mailparser'; import stripHtml from 'string-strip-html'; import { FileUpload } from '../../../app/file-upload/server'; -import { Livechat } from '../../../app/livechat/server/lib/Livechat'; import { Livechat as LivechatTyped } from '../../../app/livechat/server/lib/LivechatTyped'; import { QueueManager } from '../../../app/livechat/server/lib/QueueManager'; import { settings } from '../../../app/settings/server'; @@ -148,7 +147,7 @@ export async function onEmailReceived(email: ParsedMail, inbox: string, departme const rid = room?._id ?? Random.id(); const msgId = Random.id(); - Livechat.sendMessage({ + LivechatTyped.sendMessage({ guest, message: { _id: msgId, From a3b3dea4816c6a93829d5be3e56c9b68fbd9ad48 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 19 Oct 2023 18:27:43 -0700 Subject: [PATCH 37/38] regression: validateLicenseLimits not using the expected limit (#30693) --- ee/packages/license/src/license.spec.ts | 3 ++- ee/packages/license/src/license.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ee/packages/license/src/license.spec.ts b/ee/packages/license/src/license.spec.ts index b637ee33ddfd..c605d6467118 100644 --- a/ee/packages/license/src/license.spec.ts +++ b/ee/packages/license/src/license.spec.ts @@ -41,7 +41,7 @@ it('should prevent if the counter is equal or over the limit', async () => { await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true); }); -it.skip('should not prevent an action if another limit is over the limit', async () => { +it('should not prevent an action if another limit is over the limit', async () => { const licenseManager = await getReadyLicenseManager(); const license = await new MockedLicenseBuilder() @@ -63,6 +63,7 @@ it.skip('should not prevent an action if another limit is over the limit', async licenseManager.setLicenseLimitCounter('activeUsers', () => 11); licenseManager.setLicenseLimitCounter('monthlyActiveContacts', () => 2); await expect(licenseManager.shouldPreventAction('monthlyActiveContacts')).resolves.toBe(false); + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true); }); describe('Validate License Limits', () => { diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 14dceedd735a..8212a4a0da27 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -268,6 +268,7 @@ export class LicenseManager extends Emitter { ...(extraCount && { behaviors: ['prevent_action'] }), isNewLicense: false, suppressLog: !!suppressLog, + limits: [action], context: { [action]: { extraCount, From c29f5ff4172845d8c8880fd2e93fb5a16e7c8337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Thu, 19 Oct 2023 23:01:44 -0300 Subject: [PATCH 38/38] chore: bump fuselage packages (#30696) --- .../admin/info/UsagePieGraph.stories.tsx | 10 +-- .../views/room/Header/icons/Encrypted.tsx | 2 +- .../users/ActiveUsersSection.tsx | 6 +- .../users/UsersByTimeOfTheDaySection.tsx | 14 ++-- .../ee/client/views/admin/info/SeatsCard.tsx | 2 +- apps/meteor/package.json | 6 +- ee/packages/pdf-worker/package.json | 2 +- ee/packages/ui-theming/package.json | 2 +- packages/fuselage-ui-kit/package.json | 2 +- packages/gazzodown/package.json | 4 +- packages/livechat/package.json | 4 +- packages/ui-client/package.json | 2 +- packages/ui-composer/package.json | 2 +- packages/ui-video-conf/package.json | 2 +- packages/uikit-playground/package.json | 6 +- yarn.lock | 65 +++++++++---------- 16 files changed, 62 insertions(+), 69 deletions(-) diff --git a/apps/meteor/client/views/admin/info/UsagePieGraph.stories.tsx b/apps/meteor/client/views/admin/info/UsagePieGraph.stories.tsx index 12f211d47c5e..d76731f0d2c4 100644 --- a/apps/meteor/client/views/admin/info/UsagePieGraph.stories.tsx +++ b/apps/meteor/client/views/admin/info/UsagePieGraph.stories.tsx @@ -38,27 +38,27 @@ export const Animated: Story, 'size' | { total: 100, used: 0, - color: colorTokens.s500, + color: colorTokens.g500, }, { total: 100, used: 25, - color: colorTokens.p500, + color: colorTokens.b500, }, { total: 100, used: 50, - color: colorTokens.w500, + color: colorTokens.y500, }, { total: 100, used: 75, - color: colorTokens['s1-500'], + color: colorTokens.o500, }, { total: 100, used: 100, - color: colorTokens.d500, + color: colorTokens.r500, }, ]); diff --git a/apps/meteor/client/views/room/Header/icons/Encrypted.tsx b/apps/meteor/client/views/room/Header/icons/Encrypted.tsx index dbfda21f5b7a..bd380c5d8af2 100644 --- a/apps/meteor/client/views/room/Header/icons/Encrypted.tsx +++ b/apps/meteor/client/views/room/Header/icons/Encrypted.tsx @@ -29,7 +29,7 @@ const Encrypted = ({ room }: { room: IRoom }) => { }); }); return e2eEnabled && room?.encrypted ? ( - + ) : null; }; diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx b/apps/meteor/ee/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx index e067f777090f..eb504033e1e6 100644 --- a/apps/meteor/ee/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx +++ b/apps/meteor/ee/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx @@ -127,7 +127,7 @@ const ActiveUsersSection = ({ timezone }: ActiveUsersSectionProps): ReactElement variation: diffDailyActiveUsers ?? 0, description: ( <> - {t('Daily_Active_Users')} + {t('Daily_Active_Users')} ), }, @@ -136,7 +136,7 @@ const ActiveUsersSection = ({ timezone }: ActiveUsersSectionProps): ReactElement variation: diffWeeklyActiveUsers ?? 0, description: ( <> - {t('Weekly_Active_Users')} + {t('Weekly_Active_Users')} ), }, @@ -203,7 +203,7 @@ const ActiveUsersSection = ({ timezone }: ActiveUsersSectionProps): ReactElement right: 0, left: 40, }} - colors={[colors.p200, colors.p300, colors.p500]} + colors={[colors.b200, colors.b300, colors.b500]} axisLeft={{ // TODO: Get it from theme tickSize: 0, diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/users/UsersByTimeOfTheDaySection.tsx b/apps/meteor/ee/client/views/admin/engagementDashboard/users/UsersByTimeOfTheDaySection.tsx index d8f13bb891a3..fa5664ebca27 100644 --- a/apps/meteor/ee/client/views/admin/engagementDashboard/users/UsersByTimeOfTheDaySection.tsx +++ b/apps/meteor/ee/client/views/admin/engagementDashboard/users/UsersByTimeOfTheDaySection.tsx @@ -119,13 +119,13 @@ const UsersByTimeOfTheDaySection = ({ timezone }: UsersByTimeOfTheDaySectionProp type: 'quantize', colors: [ // TODO: Get it from theme - colors.p100, - colors.p200, - colors.p300, - colors.p400, - colors.p500, - colors.p600, - colors.p700, + colors.b100, + colors.b200, + colors.b300, + colors.b400, + colors.b500, + colors.b600, + colors.b700, ], }} emptyColor='transparent' diff --git a/apps/meteor/ee/client/views/admin/info/SeatsCard.tsx b/apps/meteor/ee/client/views/admin/info/SeatsCard.tsx index b595dd9c1fae..804893ae8458 100644 --- a/apps/meteor/ee/client/views/admin/info/SeatsCard.tsx +++ b/apps/meteor/ee/client/views/admin/info/SeatsCard.tsx @@ -23,7 +23,7 @@ const SeatsCard = ({ seatsCap }: SeatsCardProps): ReactElement => { const isNearLimit = seatsCap && seatsCap.activeUsers / seatsCap.maxActiveUsers >= 0.8; - const color = isNearLimit ? colors.d500 : undefined; + const color = isNearLimit ? colors.r500 : undefined; return ( diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 3ee3366f47dd..18e4725ffc40 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -236,11 +236,11 @@ "@rocket.chat/favicon": "workspace:^", "@rocket.chat/forked-matrix-appservice-bridge": "^4.0.1", "@rocket.chat/forked-matrix-bot-sdk": "^0.6.0-beta.2", - "@rocket.chat/fuselage": "^0.34.0", + "@rocket.chat/fuselage": "^0.35.0", "@rocket.chat/fuselage-hooks": "^0.32.1", "@rocket.chat/fuselage-polyfills": "next", "@rocket.chat/fuselage-toastbar": "next", - "@rocket.chat/fuselage-tokens": "next", + "@rocket.chat/fuselage-tokens": "^0.32.0", "@rocket.chat/fuselage-ui-kit": "workspace:^", "@rocket.chat/gazzodown": "workspace:^", "@rocket.chat/i18n": "workspace:^", @@ -251,7 +251,7 @@ "@rocket.chat/license": "workspace:^", "@rocket.chat/log-format": "workspace:^", "@rocket.chat/logger": "workspace:^", - "@rocket.chat/logo": "^0.31.27", + "@rocket.chat/logo": "^0.31.28", "@rocket.chat/memo": "next", "@rocket.chat/message-parser": "next", "@rocket.chat/model-typings": "workspace:^", diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index 9081c64fba34..f4bd7c5b44f6 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -34,7 +34,7 @@ "dependencies": { "@react-pdf/renderer": "^3.1.12", "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/fuselage-tokens": "next", + "@rocket.chat/fuselage-tokens": "^0.32.0", "@types/react": "~17.0.62", "emoji-assets": "^7.0.1", "emoji-toolkit": "^7.0.1", diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index 11aa5fd57ff8..52b8062f332d 100644 --- a/ee/packages/ui-theming/package.json +++ b/ee/packages/ui-theming/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@rocket.chat/css-in-js": "next", - "@rocket.chat/fuselage": "^0.34.0", + "@rocket.chat/fuselage": "^0.35.0", "@rocket.chat/fuselage-hooks": "^0.32.1", "@rocket.chat/icons": "^0.32.0", "@rocket.chat/ui-contexts": "workspace:~", diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 4555216c2d44..a32d2456752b 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -56,7 +56,7 @@ "devDependencies": { "@rocket.chat/apps-engine": "1.41.0-alpha.290", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.34.0", + "@rocket.chat/fuselage": "^0.35.0", "@rocket.chat/fuselage-hooks": "^0.32.1", "@rocket.chat/fuselage-polyfills": "next", "@rocket.chat/icons": "^0.32.0", diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index e891e5677c75..311950f2222d 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -6,8 +6,8 @@ "@babel/core": "~7.22.9", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "next", - "@rocket.chat/fuselage": "^0.34.0", - "@rocket.chat/fuselage-tokens": "next", + "@rocket.chat/fuselage": "^0.35.0", + "@rocket.chat/fuselage-tokens": "^0.32.0", "@rocket.chat/message-parser": "next", "@rocket.chat/styled": "next", "@rocket.chat/ui-client": "workspace:^", diff --git a/packages/livechat/package.json b/packages/livechat/package.json index 756248c1df0c..bb92a0716669 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -30,8 +30,8 @@ "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/ddp-client": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage-tokens": "next", - "@rocket.chat/logo": "^0.31.27", + "@rocket.chat/fuselage-tokens": "^0.32.0", + "@rocket.chat/logo": "^0.31.28", "@storybook/addon-essentials": "~6.5.16", "@storybook/addon-postcss": "~2.0.0", "@storybook/preact": "~6.5.16", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index f8ef2d3a1e93..7edd02cc6e56 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -5,7 +5,7 @@ "devDependencies": { "@babel/core": "~7.22.9", "@rocket.chat/css-in-js": "next", - "@rocket.chat/fuselage": "^0.34.0", + "@rocket.chat/fuselage": "^0.35.0", "@rocket.chat/fuselage-hooks": "^0.32.1", "@rocket.chat/icons": "^0.32.0", "@rocket.chat/mock-providers": "workspace:^", diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index b5c804b4a2ad..b13328bd001b 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -5,7 +5,7 @@ "devDependencies": { "@babel/core": "~7.22.9", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.34.0", + "@rocket.chat/fuselage": "^0.35.0", "@rocket.chat/icons": "^0.32.0", "@storybook/addon-actions": "~6.5.16", "@storybook/addon-docs": "~6.5.16", diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 7ca7b1d86140..2e880b3db8bb 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.9", "@rocket.chat/css-in-js": "next", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.34.0", + "@rocket.chat/fuselage": "^0.35.0", "@rocket.chat/fuselage-hooks": "^0.32.1", "@rocket.chat/icons": "^0.32.0", "@rocket.chat/styled": "next", diff --git a/packages/uikit-playground/package.json b/packages/uikit-playground/package.json index 5a8b1276defb..d9abdf001162 100644 --- a/packages/uikit-playground/package.json +++ b/packages/uikit-playground/package.json @@ -15,13 +15,13 @@ "@codemirror/tooltip": "^0.19.16", "@lezer/highlight": "^1.1.6", "@rocket.chat/css-in-js": "next", - "@rocket.chat/fuselage": "^0.34.0", + "@rocket.chat/fuselage": "^0.35.0", "@rocket.chat/fuselage-hooks": "^0.32.1", "@rocket.chat/fuselage-polyfills": "next", - "@rocket.chat/fuselage-tokens": "next", + "@rocket.chat/fuselage-tokens": "^0.32.0", "@rocket.chat/fuselage-ui-kit": "workspace:~", "@rocket.chat/icons": "^0.32.0", - "@rocket.chat/logo": "^0.31.27", + "@rocket.chat/logo": "^0.31.28", "@rocket.chat/styled": "next", "@rocket.chat/ui-contexts": "workspace:~", "codemirror": "^6.0.1", diff --git a/yarn.lock b/yarn.lock index 4b4fd7f27bc9..dc01a3898eba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8219,17 +8219,10 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/fuselage-tokens@npm:^0.31.25": - version: 0.31.25 - resolution: "@rocket.chat/fuselage-tokens@npm:0.31.25" - checksum: d05460f2f7b7f01b1498aab6fb7d932b7d752d55ce5a6bad6e7a42f2c1f056164ff8caa7dd8ec11bc0f4441a83d8aad0b8aab5e02c03f3452c4583d159b1a2f7 - languageName: node - linkType: hard - -"@rocket.chat/fuselage-tokens@npm:next": - version: 0.32.0-dev.379 - resolution: "@rocket.chat/fuselage-tokens@npm:0.32.0-dev.379" - checksum: c5cf40295c4ae1a5918651b9e156629d6400d5823b8cf5f81a14c66da986a9302d79392b45e991c2fc37aad9633f3d8e2f7f29c68969592340b05968265244e6 +"@rocket.chat/fuselage-tokens@npm:^0.32.0": + version: 0.32.0 + resolution: "@rocket.chat/fuselage-tokens@npm:0.32.0" + checksum: 8da7836877ba93462f90d13de6d3d3add8b2758b58c7988e14a8f0deffd1ceef0547f26d4c60a7ddc881e21e3327b5a04cbf17336e5ca8ab9c19789d8e6af3c0 languageName: node linkType: hard @@ -8239,7 +8232,7 @@ __metadata: dependencies: "@rocket.chat/apps-engine": 1.41.0-alpha.290 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.34.0 + "@rocket.chat/fuselage": ^0.35.0 "@rocket.chat/fuselage-hooks": ^0.32.1 "@rocket.chat/fuselage-polyfills": next "@rocket.chat/gazzodown": "workspace:^" @@ -8288,13 +8281,13 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/fuselage@npm:^0.34.0": - version: 0.34.0 - resolution: "@rocket.chat/fuselage@npm:0.34.0" +"@rocket.chat/fuselage@npm:^0.35.0": + version: 0.35.0 + resolution: "@rocket.chat/fuselage@npm:0.35.0" dependencies: "@rocket.chat/css-in-js": ^0.31.25 "@rocket.chat/css-supports": ^0.31.25 - "@rocket.chat/fuselage-tokens": ^0.31.25 + "@rocket.chat/fuselage-tokens": ^0.32.0 "@rocket.chat/memo": ^0.31.25 "@rocket.chat/styled": ^0.31.25 invariant: ^2.2.4 @@ -8308,7 +8301,7 @@ __metadata: react: ^17.0.2 react-dom: ^17.0.2 react-virtuoso: 1.2.4 - checksum: 72cd1dd7ef13cc3b69fadac5c064a45cd2b65b8a221cde2e8149fa873ac6de89648c677caedb10979e5cf08d39b79f1d7a30caa6378bdeeb873414c7fbac5e6e + checksum: 46deea587a1ab4c80a25f4e93882905e2f24778c0e612b7cdd18bfb0c72b2c079d4eee6fe7ad4c52a62354197ebed0a62eaf939b5714859b7086c923668f3f05 languageName: node linkType: hard @@ -8319,8 +8312,8 @@ __metadata: "@babel/core": ~7.22.9 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": next - "@rocket.chat/fuselage": ^0.34.0 - "@rocket.chat/fuselage-tokens": next + "@rocket.chat/fuselage": ^0.35.0 + "@rocket.chat/fuselage-tokens": ^0.32.0 "@rocket.chat/message-parser": next "@rocket.chat/styled": next "@rocket.chat/ui-client": "workspace:^" @@ -8468,9 +8461,9 @@ __metadata: "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/ddp-client": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage-tokens": next + "@rocket.chat/fuselage-tokens": ^0.32.0 "@rocket.chat/gazzodown": "workspace:^" - "@rocket.chat/logo": ^0.31.27 + "@rocket.chat/logo": ^0.31.28 "@rocket.chat/message-parser": next "@rocket.chat/random": "workspace:~" "@rocket.chat/sdk": ^1.0.0-alpha.42 @@ -8581,16 +8574,16 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/logo@npm:^0.31.27": - version: 0.31.27 - resolution: "@rocket.chat/logo@npm:0.31.27" +"@rocket.chat/logo@npm:^0.31.28": + version: 0.31.28 + resolution: "@rocket.chat/logo@npm:0.31.28" dependencies: "@rocket.chat/fuselage-hooks": ^0.32.1 "@rocket.chat/styled": ^0.31.25 peerDependencies: react: 17.0.2 react-dom: 17.0.2 - checksum: acc56410813a0d4f634f9e847bc4b49275c26aff4e2f285720818cb012a2ad42554982fcc4078c485222a9c9a78244d1a4b16b60588b5c50441b8928c3957efb + checksum: 2ba185326fadb0d1ccf7d2767435204dd3cd857400d18e59eb8a07055ac0183c6e780d0e8e45436410c551aef516ecea7491a5c87b59406252b2be4694034af8 languageName: node linkType: hard @@ -8665,11 +8658,11 @@ __metadata: "@rocket.chat/favicon": "workspace:^" "@rocket.chat/forked-matrix-appservice-bridge": ^4.0.1 "@rocket.chat/forked-matrix-bot-sdk": ^0.6.0-beta.2 - "@rocket.chat/fuselage": ^0.34.0 + "@rocket.chat/fuselage": ^0.35.0 "@rocket.chat/fuselage-hooks": ^0.32.1 "@rocket.chat/fuselage-polyfills": next "@rocket.chat/fuselage-toastbar": next - "@rocket.chat/fuselage-tokens": next + "@rocket.chat/fuselage-tokens": ^0.32.0 "@rocket.chat/fuselage-ui-kit": "workspace:^" "@rocket.chat/gazzodown": "workspace:^" "@rocket.chat/i18n": "workspace:^" @@ -8681,7 +8674,7 @@ __metadata: "@rocket.chat/livechat": "workspace:^" "@rocket.chat/log-format": "workspace:^" "@rocket.chat/logger": "workspace:^" - "@rocket.chat/logo": ^0.31.27 + "@rocket.chat/logo": ^0.31.28 "@rocket.chat/memo": next "@rocket.chat/message-parser": next "@rocket.chat/mock-providers": "workspace:^" @@ -9175,7 +9168,7 @@ __metadata: dependencies: "@react-pdf/renderer": ^3.1.12 "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/fuselage-tokens": next + "@rocket.chat/fuselage-tokens": ^0.32.0 "@storybook/addon-essentials": ~6.5.16 "@storybook/react": ~6.5.16 "@testing-library/jest-dom": ^5.16.5 @@ -9528,7 +9521,7 @@ __metadata: dependencies: "@babel/core": ~7.22.9 "@rocket.chat/css-in-js": next - "@rocket.chat/fuselage": ^0.34.0 + "@rocket.chat/fuselage": ^0.35.0 "@rocket.chat/fuselage-hooks": ^0.32.1 "@rocket.chat/icons": ^0.32.0 "@rocket.chat/mock-providers": "workspace:^" @@ -9579,7 +9572,7 @@ __metadata: dependencies: "@babel/core": ~7.22.9 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.34.0 + "@rocket.chat/fuselage": ^0.35.0 "@rocket.chat/icons": ^0.32.0 "@storybook/addon-actions": ~6.5.16 "@storybook/addon-docs": ~6.5.16 @@ -9650,7 +9643,7 @@ __metadata: resolution: "@rocket.chat/ui-theming@workspace:ee/packages/ui-theming" dependencies: "@rocket.chat/css-in-js": next - "@rocket.chat/fuselage": ^0.34.0 + "@rocket.chat/fuselage": ^0.35.0 "@rocket.chat/fuselage-hooks": ^0.32.1 "@rocket.chat/icons": ^0.32.0 "@rocket.chat/ui-contexts": "workspace:~" @@ -9693,7 +9686,7 @@ __metadata: "@rocket.chat/css-in-js": next "@rocket.chat/emitter": next "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.34.0 + "@rocket.chat/fuselage": ^0.35.0 "@rocket.chat/fuselage-hooks": ^0.32.1 "@rocket.chat/icons": ^0.32.0 "@rocket.chat/styled": next @@ -9736,13 +9729,13 @@ __metadata: "@codemirror/tooltip": ^0.19.16 "@lezer/highlight": ^1.1.6 "@rocket.chat/css-in-js": next - "@rocket.chat/fuselage": ^0.34.0 + "@rocket.chat/fuselage": ^0.35.0 "@rocket.chat/fuselage-hooks": ^0.32.1 "@rocket.chat/fuselage-polyfills": next - "@rocket.chat/fuselage-tokens": next + "@rocket.chat/fuselage-tokens": ^0.32.0 "@rocket.chat/fuselage-ui-kit": "workspace:~" "@rocket.chat/icons": ^0.32.0 - "@rocket.chat/logo": ^0.31.27 + "@rocket.chat/logo": ^0.31.28 "@rocket.chat/styled": next "@rocket.chat/ui-contexts": "workspace:~" "@types/react": ~17.0.62