From 5224eab2982965e3df15a97226dd32b59218aa22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Mon, 4 Dec 2023 14:15:22 -0300 Subject: [PATCH 1/9] feat: Image gallery with swiper (#30623) --- apps/meteor/.meteor/packages | 1 - apps/meteor/.meteor/versions | 1 - apps/meteor/app/api/server/v1/channels.ts | 45 +- .../emoji-custom/client/lib/emojiCustom.js | 46 -- apps/meteor/app/theme/client/main.css | 1 - .../app/theme/client/vendor/photoswipe.css | 422 ------------------ apps/meteor/app/ui/client/index.ts | 1 - .../ui/client/views/app/photoswipeContent.ts | 203 --------- .../components/ImageGallery/ImageGallery.tsx | 138 ++++++ .../ImageGallery/ImageGalleryLoader.tsx | 22 + .../ImageGallery/hooks/useImageGallery.ts | 21 + .../ImageGallery/hooks/useImagesList.ts | 69 +++ .../client/components/ImageGallery/index.ts | 1 + .../message/content/Attachments.tsx | 5 +- .../content/attachments/AttachmentsItem.tsx | 5 +- .../content/attachments/FileAttachment.tsx | 2 +- .../attachments/file/ImageAttachment.tsx | 4 +- .../attachments/structure/AttachmentImage.tsx | 12 +- .../variants/room/RoomMessageContent.tsx | 2 +- .../client/contexts/ImageGalleryContext.ts | 13 + apps/meteor/client/importPackages.ts | 1 - apps/meteor/client/lib/lists/ImagesList.ts | 39 ++ .../client/providers/ImageGalleryProvider.tsx | 44 ++ .../moderation/helpers/ContextMessage.tsx | 2 +- .../RoomFiles/components/FileItem.js | 54 +-- .../RoomFiles/components/ImageItem.tsx | 34 ++ .../RoomFiles/hooks/useFilesList.ts | 2 +- .../views/room/providers/RoomProvider.tsx | 5 +- apps/meteor/package.json | 4 +- apps/meteor/server/models/raw/Uploads.ts | 25 +- .../model-typings/src/models/IUploadsModel.ts | 10 +- .../src/v1/channels/ChannelsImagesProps.ts | 14 + .../rest-typings/src/v1/channels/channels.ts | 6 + .../rest-typings/src/v1/channels/index.ts | 1 + yarn.lock | 55 +-- 35 files changed, 559 insertions(+), 751 deletions(-) delete mode 100644 apps/meteor/app/theme/client/vendor/photoswipe.css delete mode 100644 apps/meteor/app/ui/client/index.ts delete mode 100644 apps/meteor/app/ui/client/views/app/photoswipeContent.ts create mode 100644 apps/meteor/client/components/ImageGallery/ImageGallery.tsx create mode 100644 apps/meteor/client/components/ImageGallery/ImageGalleryLoader.tsx create mode 100644 apps/meteor/client/components/ImageGallery/hooks/useImageGallery.ts create mode 100644 apps/meteor/client/components/ImageGallery/hooks/useImagesList.ts create mode 100644 apps/meteor/client/components/ImageGallery/index.ts create mode 100644 apps/meteor/client/contexts/ImageGalleryContext.ts create mode 100644 apps/meteor/client/lib/lists/ImagesList.ts create mode 100644 apps/meteor/client/providers/ImageGalleryProvider.tsx create mode 100644 apps/meteor/client/views/room/contextualBar/RoomFiles/components/ImageItem.tsx create mode 100644 packages/rest-typings/src/v1/channels/ChannelsImagesProps.ts diff --git a/apps/meteor/.meteor/packages b/apps/meteor/.meteor/packages index ae788af78034..e99aa0bdfb67 100644 --- a/apps/meteor/.meteor/packages +++ b/apps/meteor/.meteor/packages @@ -72,6 +72,5 @@ autoupdate@1.8.0 # photoswipe -jquery zodern:types zodern:standard-minifier-js diff --git a/apps/meteor/.meteor/versions b/apps/meteor/.meteor/versions index 66f61e2cd8cc..6dab889e3e38 100644 --- a/apps/meteor/.meteor/versions +++ b/apps/meteor/.meteor/versions @@ -43,7 +43,6 @@ hot-code-push@1.0.4 http@2.0.0 id-map@1.1.1 inter-process-messaging@0.1.1 -jquery@3.0.0 kadira:flow-router@2.12.1 localstorage@1.2.0 logging@1.3.2 diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index 8e0541b8040b..1c84926edb63 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -1,5 +1,5 @@ import { Team, Room } from '@rocket.chat/core-services'; -import type { IRoom, ISubscription, IUser, RoomType } from '@rocket.chat/core-typings'; +import type { IRoom, ISubscription, IUser, RoomType, IUpload } from '@rocket.chat/core-typings'; import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models'; import { isChannelsAddAllProps, @@ -18,6 +18,7 @@ import { isChannelsConvertToTeamProps, isChannelsSetReadOnlyProps, isChannelsDeleteProps, + isChannelsImagesProps, } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; @@ -803,6 +804,48 @@ API.v1.addRoute( }, ); +API.v1.addRoute( + 'channels.images', + { authRequired: true, validateParams: isChannelsImagesProps }, + { + async get() { + const room = await Rooms.findOneById>(this.queryParams.roomId, { + projection: { t: 1, teamId: 1, prid: 1 }, + }); + + if (!room || !(await canAccessRoomAsync(room, { _id: this.userId }))) { + return API.v1.unauthorized(); + } + + let initialImage: IUpload | null = null; + if (this.queryParams.startingFromId) { + initialImage = await Uploads.findOneById(this.queryParams.startingFromId); + } + + const { offset, count } = await getPaginationItems(this.queryParams); + + const { cursor, totalCount } = Uploads.findImagesByRoomId(room._id, initialImage?.uploadedAt, { + skip: offset, + limit: count, + }); + + const [files, total] = await Promise.all([cursor.toArray(), totalCount]); + + // If the initial image was not returned in the query, insert it as the first element of the list + if (initialImage && !files.find(({ _id }) => _id === (initialImage as IUpload)._id)) { + files.splice(0, 0, initialImage); + } + + return API.v1.success({ + files, + count, + offset, + total, + }); + }, + }, +); + API.v1.addRoute( 'channels.getIntegrations', { authRequired: true }, diff --git a/apps/meteor/app/emoji-custom/client/lib/emojiCustom.js b/apps/meteor/app/emoji-custom/client/lib/emojiCustom.js index 1340ead95789..64f1df9bd932 100644 --- a/apps/meteor/app/emoji-custom/client/lib/emojiCustom.js +++ b/apps/meteor/app/emoji-custom/client/lib/emojiCustom.js @@ -1,11 +1,9 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; -import $ from 'jquery'; import { Meteor } from 'meteor/meteor'; import { Session } from 'meteor/session'; import { emoji, updateRecent } from '../../../emoji/client'; import { CachedCollectionManager } from '../../../ui-cached-collection/client'; -import { LegacyRoomManager } from '../../../ui-utils/client'; import { getURL } from '../../../utils/client'; import { sdk } from '../../../utils/client/lib/SDKClient'; import { isSetNotNull } from './function-isSet'; @@ -45,9 +43,6 @@ export const deleteEmojiCustom = function (emojiData) { }; export const updateEmojiCustom = function (emojiData) { - let key = `emoji_random_${emojiData.name}`; - Session.set(key, Math.round(Math.random() * 1000)); - const previousExists = isSetNotNull(() => emojiData.previousName); const currentAliases = isSetNotNull(() => emojiData.aliases); @@ -88,47 +83,6 @@ export const updateEmojiCustom = function (emojiData) { } } - const url = getEmojiUrlFromName(emojiData.name, emojiData.extension); - - // update in admin interface - if (previousExists && emojiData.name !== emojiData.previousName) { - $(document) - .find(`.emojiAdminPreview-image[data-emoji='${emojiData.previousName}']`) - .css('background-image', `url('${url})'`) - .attr('data-emoji', `${emojiData.name}`); - } else { - $(document).find(`.emojiAdminPreview-image[data-emoji='${emojiData.name}']`).css('background-image', `url('${url}')`); - } - - // update in picker - if (previousExists && emojiData.name !== emojiData.previousName) { - $(document) - .find(`li[data-emoji='${emojiData.previousName}'] span`) - .css('background-image', `url('${url}')`) - .attr('data-emoji', `${emojiData.name}`); - $(document) - .find(`li[data-emoji='${emojiData.previousName}']`) - .attr('data-emoji', `${emojiData.name}`) - .attr('class', `emoji-${emojiData.name}`); - } else { - $(document).find(`li[data-emoji='${emojiData.name}'] span`).css('background-image', `url('${url}')`); - } - - // update in picker and opened rooms - for (key in LegacyRoomManager.openedRooms) { - if (LegacyRoomManager.openedRooms.hasOwnProperty(key)) { - const room = LegacyRoomManager.openedRooms[key]; - if (previousExists && emojiData.name !== emojiData.previousName) { - $(room.dom) - .find(`span[data-emoji='${emojiData.previousName}']`) - .css('background-image', `url('${url}')`) - .attr('data-emoji', `${emojiData.name}`); - } else { - $(room.dom).find(`span[data-emoji='${emojiData.name}']`).css('background-image', `url('${url}')`); - } - } - } - updateRecent('rocket'); }; diff --git a/apps/meteor/app/theme/client/main.css b/apps/meteor/app/theme/client/main.css index 6c0a20d844bb..33b7a8f0d290 100644 --- a/apps/meteor/app/theme/client/main.css +++ b/apps/meteor/app/theme/client/main.css @@ -21,7 +21,6 @@ /* Legacy theming */ @import 'imports/general/theme_old.css'; -@import './vendor/photoswipe.css'; @import './vendor/fontello/css/fontello.css'; @import './rocketchat.font.css'; @import './mentionLink.css'; diff --git a/apps/meteor/app/theme/client/vendor/photoswipe.css b/apps/meteor/app/theme/client/vendor/photoswipe.css deleted file mode 100644 index 53f352ff7018..000000000000 --- a/apps/meteor/app/theme/client/vendor/photoswipe.css +++ /dev/null @@ -1,422 +0,0 @@ -.pswp__button { - position: relative; - - display: block; - float: right; - overflow: visible; - - width: 44px; - height: 44px; - margin: 0; - padding: 0; - - cursor: pointer; - transition: opacity 0.2s; - - opacity: 0.75; - border: 0; - background: none; - box-shadow: none; - appearance: none; - - &:focus, - &:hover { - opacity: 1; - } - - &:active { - opacity: 0.9; - outline: none; - } - - &::-moz-focus-inner { - padding: 0; - - border: 0; - } -} - -.pswp__ui--over-close .pswp__button--close { - opacity: 1; -} - -.pswp__button, -.pswp__button--arrow--left::before, -.pswp__button--arrow--right::before { - width: 44px; - height: 44px; - - background: url(/images/gallery-skin.svg) 0 0 no-repeat; - background-size: 264px 88px; -} - -@media (-webkit-min-device-pixel-ratio: 1.1), - (-webkit-min-device-pixel-ratio: 1.09375), - (min-resolution: 105dpi), - (min-resolution: 1.1dppx) { - .pswp--svg .pswp__button--arrow--left, - .pswp--svg .pswp__button--arrow--right { - background: none; - } -} - -.pswp__button--close { - background-position: 0 -44px; -} - -.pswp__button--share { - background-position: -44px -44px; -} - -.pswp__button--fs { - display: none; -} - -.pswp--supports-fs .pswp__button--fs { - display: block; -} - -.pswp--fs .pswp__button--fs { - background-position: -44px 0; -} - -.pswp__button--zoom { - display: none; - - background-position: -88px 0; -} - -.pswp--zoom-allowed .pswp__button--zoom { - display: block; -} - -.pswp--zoomed-in .pswp__button--zoom { - background-position: -132px 0; -} - -.pswp--touch .pswp__button--arrow--left, -.pswp--touch .pswp__button--arrow--right { - visibility: hidden; -} - -.pswp__button--arrow--left, -.pswp__button--arrow--right { - position: absolute; - top: 50%; - - width: 70px; - height: 100px; - margin-top: -50px; - - background: none; -} - -.pswp__button--arrow--left { - left: 0; -} - -.pswp__button--arrow--right { - right: 0; -} - -.pswp__button--arrow--left::before, -.pswp__button--arrow--right::before { - position: absolute; - top: 35px; - - width: 32px; - height: 30px; - - content: ''; - - background-color: rgba(0, 0, 0, 0.3); -} - -.pswp__button--arrow--left::before { - left: 6px; - - background-position: -138px -44px; -} - -.pswp__button--arrow--right::before { - right: 6px; - - background-position: -94px -44px; -} - -/* - - 4. Caption - - */ -.pswp__caption { - position: absolute; - bottom: 0; - left: 0; - - width: 100%; - min-height: 44px; - - & small { - color: #bbbbbb; - - font-size: 11px; - } -} - -.pswp__caption__center { - max-width: 420px; - margin: 0 auto; - padding: 10px; - - text-align: left; - - color: #cccccc; - - font-size: 13px; - line-height: 20px; -} - -.pswp__caption--empty { - display: none; -} - -/* Fake caption element, used to calculate height of next/prev image */ -.pswp__caption--fake { - visibility: hidden; -} - -/* - - 5. Loading indicator (preloader) - - You can play with it here - http://codepen.io/dimsemenov/pen/yyBWoR - - */ -.pswp__preloader { - position: absolute; - top: 0; - left: 50%; - - width: 44px; - height: 44px; - margin-left: -22px; - - transition: opacity 0.25s ease-out; - - opacity: 0; - direction: ltr; - will-change: opacity; -} - -.pswp__preloader__icn { - width: 20px; - height: 20px; - margin: 12px; -} - -.pswp__preloader--active { - opacity: 1; -} - -.pswp--css_animation { - & .pswp__preloader--active { - opacity: 1; - - & .pswp__preloader__icn { - position: absolute; - top: 15px; - left: 15px; - - width: 14px; - height: 14px; - margin: 0; - - animation: clockwise 500ms linear infinite; - - opacity: 0.75; - background: none; - } - - & .pswp__preloader__donut { - position: absolute; - top: 0; - left: 0; - - box-sizing: border-box; - width: 14px; - height: 14px; - margin: 0; - - animation: donut-rotate 1000ms cubic-bezier(0.4, 0, 0.22, 1) infinite; - - border: 1px solid #ffffff; - border-bottom-color: transparent; - border-left-color: transparent; - border-radius: 50%; - background: none; - } - - & .pswp__preloader__cut { - /* - The idea of animating inner circle is based on Polymer ("material") loading indicator - by Keanu Lee https://blog.keanulee.com/2014/10/20/the-tale-of-three-spinners.html - */ - position: relative; - - overflow: hidden; - - width: 7px; - height: 14px; - } - } -} - -@media screen and (max-width: 1024px) { - .pswp__preloader { - position: relative; - top: auto; - left: auto; - - float: right; - - margin: 0; - } -} - -@-webkit-keyframes clockwise { - 0% { - transform: rotate(0deg); - } - - 100% { - transform: rotate(360deg); - } -} - -@keyframes clockwise { - 0% { - transform: rotate(0deg); - } - - 100% { - transform: rotate(360deg); - } -} - -@-webkit-keyframes donut-rotate { - 0% { - transform: rotate(0); - } - - 50% { - transform: rotate(-140deg); - } - - 100% { - transform: rotate(0); - } -} - -@keyframes donut-rotate { - 0% { - transform: rotate(0); - } - - 50% { - transform: rotate(-140deg); - } - - 100% { - transform: rotate(0); - } -} - -/* - - 6. Additional styles - - */ - -/* root element of UI */ -.pswp__ui { - z-index: 1550; - - visibility: visible; - - opacity: 1; - -webkit-font-smoothing: auto; -} - -/* top black bar with buttons and "1 of X" indicator */ -.pswp__top-bar { - position: absolute; - top: 0; - left: 0; - - width: 100%; - height: 44px; -} - -.pswp__caption, -.pswp__top-bar, -.pswp--has_mouse .pswp__button--arrow--left, -.pswp--has_mouse .pswp__button--arrow--right { - transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1); - -webkit-backface-visibility: hidden; - will-change: opacity; -} - -/* pswp--has_mouse class is added only when two subsequent mousemove events occur */ -.pswp--has_mouse .pswp__button--arrow--left, -.pswp--has_mouse .pswp__button--arrow--right { - visibility: visible; -} - -.pswp__top-bar, -.pswp__caption { - background-color: rgba(0, 0, 0, 0.5); -} - -/* pswp__ui--fit class is added when main image "fits" between top bar and bottom bar (caption) */ -.pswp__ui--fit .pswp__top-bar, -.pswp__ui--fit .pswp__caption { - background-color: rgba(0, 0, 0, 0.3); -} - -/* pswp__ui--idle class is added when mouse isn't moving for several seconds (JS option timeToIdle) */ -.pswp__ui--idle .pswp__top-bar { - opacity: 0; -} - -.pswp__ui--idle .pswp__button--arrow--left, -.pswp__ui--idle .pswp__button--arrow--right { - opacity: 0; -} - -/* - pswp__ui--hidden class is added when controls are hidden - e.g. when user taps to toggle visibility of controls -*/ -.pswp__ui--hidden .pswp__top-bar, -.pswp__ui--hidden .pswp__caption, -.pswp__ui--hidden .pswp__button--arrow--left, -.pswp__ui--hidden .pswp__button--arrow--right { - /* Force paint & create composition layer for controls. */ - opacity: 0.001; -} - -/* pswp__ui--one-slide class is added when there is just one item in gallery */ -.pswp__ui--one-slide .pswp__button--arrow--left, -.pswp__ui--one-slide .pswp__button--arrow--right, -.pswp__ui--one-slide .pswp__counter { - display: none; -} - -.pswp__element--disabled { - display: none !important; -} - -.pswp--minimal--dark .pswp__top-bar { - background: none; -} diff --git a/apps/meteor/app/ui/client/index.ts b/apps/meteor/app/ui/client/index.ts deleted file mode 100644 index 7a2b4aaa89b7..000000000000 --- a/apps/meteor/app/ui/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './views/app/photoswipeContent.ts'; // without the *.ts extension, *.html gets loaded first diff --git a/apps/meteor/app/ui/client/views/app/photoswipeContent.ts b/apps/meteor/app/ui/client/views/app/photoswipeContent.ts deleted file mode 100644 index 5c76a6b15d18..000000000000 --- a/apps/meteor/app/ui/client/views/app/photoswipeContent.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { escapeHTML } from '@rocket.chat/string-helpers'; -import { Meteor } from 'meteor/meteor'; -import type PhotoSwipe from 'photoswipe'; -import PhotoSwipeUIDefault from 'photoswipe/dist/photoswipe-ui-default'; - -import { createAnchor } from '../../../../../client/lib/utils/createAnchor'; - -const parseLength = (x: unknown): number | undefined => { - const length = typeof x === 'string' ? parseInt(x, 10) : undefined; - return Number.isFinite(length) ? length : undefined; -}; - -const getImageSize = (src: string): Promise<[w: number, h: number]> => - new Promise((resolve, reject) => { - const img = new Image(); - - img.addEventListener('load', () => { - resolve([img.naturalWidth, img.naturalHeight]); - }); - - img.addEventListener('error', (error) => { - reject(error.error); - }); - - img.src = src; - }); - -type Slide = PhotoSwipe.Item & { description?: string; title?: string }; - -const fromElementToSlide = async (element: Element): Promise => { - if (!(element instanceof HTMLElement)) { - return null; - } - - const title = element.dataset.title || element.title; - const { description } = element.dataset; - - if (element instanceof HTMLAnchorElement) { - const src = element.dataset.src || element.href; - let w = parseLength(element.dataset.width); - let h = parseLength(element.dataset.height); - - if (w === undefined || h === undefined) { - [w, h] = await getImageSize(src); - } - - return { src, w, h, title, description }; - } - - if (element instanceof HTMLImageElement) { - let msrc: string | undefined; - let { src } = element; - let w: number | undefined = element.naturalWidth; - let h: number | undefined = element.naturalHeight; - - if (element.dataset.src) { - msrc = element.src; - src = element.dataset.src; - w = parseLength(element.dataset.width); - h = parseLength(element.dataset.height); - - if (w === undefined || h === undefined) { - [w, h] = await getImageSize(src); - } - } - - return { msrc, src, w, h, title, description }; - } - - return null; -}; - -let currentGallery: PhotoSwipe | null = null; - -const initGallery = async (items: Slide[], options: PhotoSwipe.Options): Promise => { - const anchor = createAnchor('photoswipe-root'); - - anchor.innerHTML = ``; - const [{ default: PhotoSwipe }] = await Promise.all([import('photoswipe'), import('photoswipe/dist/photoswipe.css')]); - - if (!currentGallery) { - const container = document.getElementById('pswp'); - - if (!container) { - throw new Error('Photoswipe container element not found'); - } - - currentGallery = new PhotoSwipe(container, PhotoSwipeUIDefault, items, options); - - currentGallery.listen('destroy', () => { - anchor.innerHTML = ''; - currentGallery = null; - }); - - currentGallery.init(); - } -}; - -const defaultGalleryOptions = { - bgOpacity: 0.7, - counterEl: false, - index: 0, - wheelToZoom: true, - padding: { top: 20, bottom: 40, left: 100, right: 100 }, - addCaptionHTMLFn(item: Slide, captionEl: HTMLElement): boolean { - captionEl.children[0].innerHTML = ` - ${escapeHTML(item.title ?? '')}
- ${escapeHTML(item.description ?? '')} - `; - return true; - }, -}; - -const createEventListenerFor = - (className: string) => - (event: Event): void => { - event.preventDefault(); - event.stopPropagation(); - - const { currentTarget } = event; - - const galleryItems = Array.from(document.querySelectorAll(className)); - - const sortedElements = galleryItems.sort((a, b) => { - if (a === currentTarget) { - return -1; - } - - if (b === currentTarget) { - return 1; - } - - return 0; - }); - - const slidePromises = sortedElements.map((element) => fromElementToSlide(element)); - - let hasOpenedGallery = false; - - void slidePromises.reduce( - (p, curr) => - p - .then(() => curr) - .then(async (slide) => { - if (!slide) { - return; - } - - if (!currentGallery) { - // If the gallery doesn't exist and has been opened this run the user closed it before all promises ran. - // This means it shouldn't be opened again. - if (hasOpenedGallery) { - return; - } - hasOpenedGallery = true; - return initGallery([slide], defaultGalleryOptions); - } - - currentGallery.items.push(slide); - currentGallery.invalidateCurrItems(); - currentGallery.updateSize(true); - }), - Promise.resolve(), - ); - }; - -Meteor.startup(() => { - $(document).on('click', '.gallery-item', createEventListenerFor('.gallery-item')); -}); diff --git a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx new file mode 100644 index 000000000000..2946e3e692a2 --- /dev/null +++ b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx @@ -0,0 +1,138 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box, IconButton, Throbber } from '@rocket.chat/fuselage'; +import React, { useRef, useState } from 'react'; +import { FocusScope } from 'react-aria'; +import { createPortal } from 'react-dom'; +import { Keyboard, Navigation, Zoom, A11y } from 'swiper'; +import type { SwiperRef } from 'swiper/react'; +import { type SwiperClass, Swiper, SwiperSlide } from 'swiper/react'; + +// Import Swiper styles +import 'swiper/swiper.css'; +import 'swiper/modules/navigation/navigation.min.css'; +import 'swiper/modules/keyboard/keyboard.min.css'; +import 'swiper/modules/zoom/zoom.min.css'; + +import ImageGalleryLoader from './ImageGalleryLoader'; +import { useImageGallery } from './hooks/useImageGallery'; + +const swiperStyle = css` + .swiper { + width: 100%; + height: 100%; + } + .swiper-container { + position: absolute; + z-index: 99; + top: 0; + + overflow: hidden; + + width: 100%; + height: 100%; + + background-color: var(--rcx-color-surface-overlay, rgba(0, 0, 0, 0.6)); + } + + .rcx-swiper-close-button, + .rcx-swiper-prev-button, + .rcx-swiper-next-button { + color: var(--rcx-color-font-pure-white, #ffffff) !important; + } + + .rcx-swiper-close-button { + position: absolute; + z-index: 10; + top: 10px; + right: 10px; + } + + .rcx-swiper-prev-button, + .rcx-swiper-next-button { + position: absolute; + z-index: 10; + top: 50%; + + cursor: pointer; + } + + .rcx-swiper-prev-button.swiper-button-disabled, + .rcx-swiper-next-button.swiper-button-disabled { + cursor: auto; + pointer-events: none; + + opacity: 0.35; + } + + .rcx-swiper-prev-button.swiper-button-hidden, + .rcx-swiper-next-button.swiper-button-hidden { + cursor: auto; + pointer-events: none; + + opacity: 0; + } + + .rcx-swiper-prev-button, + .swiper-rtl .rcx-swiper-next-button { + right: auto; + left: 10px; + } + + .rcx-swiper-next-button, + .swiper-rtl .rcx-swiper-prev-button { + right: 10px; + left: auto; + } +`; + +const ImageGallery = () => { + const swiperRef = useRef(null); + const [, setSwiperInst] = useState(); + + const { isLoading, loadMore, images, onClose } = useImageGallery(); + + if (isLoading) { + return ; + } + + return createPortal( + + +
+ + + + String(keyCode) === '27' && onClose()} + modules={[Navigation, Zoom, Keyboard, A11y]} + onInit={(swiper) => setSwiperInst(swiper)} + onReachEnd={loadMore} + > + {images?.map(({ _id, url }) => ( + +
+ +
+ +
+
+
+ ))} +
+
+
+
, + document.body, + ); +}; + +export default ImageGallery; diff --git a/apps/meteor/client/components/ImageGallery/ImageGalleryLoader.tsx b/apps/meteor/client/components/ImageGallery/ImageGalleryLoader.tsx new file mode 100644 index 000000000000..a495f345194e --- /dev/null +++ b/apps/meteor/client/components/ImageGallery/ImageGalleryLoader.tsx @@ -0,0 +1,22 @@ +import { css } from '@rocket.chat/css-in-js'; +import { IconButton, ModalBackdrop, Throbber } from '@rocket.chat/fuselage'; +import React from 'react'; +import { createPortal } from 'react-dom'; + +const closeButtonStyle = css` + position: absolute; + z-index: 10; + top: 10px; + right: 10px; +`; + +const ImageGalleryLoader = ({ onClose }: { onClose: () => void }) => + createPortal( + + + + , + document.body, + ); + +export default ImageGalleryLoader; diff --git a/apps/meteor/client/components/ImageGallery/hooks/useImageGallery.ts b/apps/meteor/client/components/ImageGallery/hooks/useImageGallery.ts new file mode 100644 index 000000000000..9d058a010fdc --- /dev/null +++ b/apps/meteor/client/components/ImageGallery/hooks/useImageGallery.ts @@ -0,0 +1,21 @@ +import { useMemo, useContext } from 'react'; + +import { ImageGalleryContext } from '../../../contexts/ImageGalleryContext'; +import { useRecordList } from '../../../hooks/lists/useRecordList'; +import { useRoom } from '../../../views/room/contexts/RoomContext'; +import { useImagesList } from './useImagesList'; + +export const useImageGallery = () => { + const { _id: rid } = useRoom(); + const { imageId, onClose } = useContext(ImageGalleryContext); + + const { filesList, loadMoreItems } = useImagesList(useMemo(() => ({ roomId: rid, startingFromId: imageId }), [imageId, rid])); + const { phase, items: filesItems } = useRecordList(filesList); + + return { + images: filesItems, + isLoading: phase === 'loading', + loadMore: () => loadMoreItems(filesItems.length - 1), + onClose, + }; +}; diff --git a/apps/meteor/client/components/ImageGallery/hooks/useImagesList.ts b/apps/meteor/client/components/ImageGallery/hooks/useImagesList.ts new file mode 100644 index 000000000000..da05c7880746 --- /dev/null +++ b/apps/meteor/client/components/ImageGallery/hooks/useImagesList.ts @@ -0,0 +1,69 @@ +import type { ChannelsImagesProps } from '@rocket.chat/rest-typings'; +import { useUserId, useEndpoint } from '@rocket.chat/ui-contexts'; +import { useCallback, useEffect, useState } from 'react'; + +import { useScrollableRecordList } from '../../../hooks/lists/useScrollableRecordList'; +import { useStreamUpdatesForMessageList } from '../../../hooks/lists/useStreamUpdatesForMessageList'; +import { useComponentDidUpdate } from '../../../hooks/useComponentDidUpdate'; +import { ImagesList } from '../../../lib/lists/ImagesList'; +import type { MessageList } from '../../../lib/lists/MessageList'; + +export const useImagesList = ( + options: ChannelsImagesProps, +): { + filesList: ImagesList; + initialItemCount: number; + reload: () => void; + loadMoreItems: (start: number) => void; +} => { + const [filesList, setFilesList] = useState(() => new ImagesList(options)); + const reload = useCallback(() => setFilesList(new ImagesList(options)), [options]); + const uid = useUserId(); + + useComponentDidUpdate(() => { + options && reload(); + }, [options, reload]); + + useEffect(() => { + if (filesList.options !== options) { + filesList.updateFilters(options); + } + }, [filesList, options]); + + const apiEndPoint = '/v1/channels.images'; + + const getFiles = useEndpoint('GET', apiEndPoint); + + const fetchMessages = useCallback( + async (start, end) => { + const { files, total } = await getFiles({ + roomId: options.roomId, + startingFromId: options.startingFromId, + offset: start, + count: end, + }); + + return { + items: files.map((file) => ({ + ...file, + uploadedAt: file.uploadedAt ? new Date(file.uploadedAt) : undefined, + modifiedAt: file.modifiedAt ? new Date(file.modifiedAt) : undefined, + })), + itemCount: total, + }; + }, + [getFiles, options.roomId, options.startingFromId], + ); + + const { loadMoreItems, initialItemCount } = useScrollableRecordList(filesList, fetchMessages, 5); + + // TODO: chapter day : frontend create useStreamUpdatesForUploadList + useStreamUpdatesForMessageList(filesList as unknown as MessageList, uid, options.roomId || null); + + return { + reload, + filesList, + loadMoreItems, + initialItemCount, + }; +}; diff --git a/apps/meteor/client/components/ImageGallery/index.ts b/apps/meteor/client/components/ImageGallery/index.ts new file mode 100644 index 000000000000..db657797badb --- /dev/null +++ b/apps/meteor/client/components/ImageGallery/index.ts @@ -0,0 +1 @@ +export { default } from './ImageGallery'; diff --git a/apps/meteor/client/components/message/content/Attachments.tsx b/apps/meteor/client/components/message/content/Attachments.tsx index b08215c94a5f..2c1b6675cb7b 100644 --- a/apps/meteor/client/components/message/content/Attachments.tsx +++ b/apps/meteor/client/components/message/content/Attachments.tsx @@ -7,13 +7,14 @@ import AttachmentsItem from './attachments/AttachmentsItem'; type AttachmentsProps = { attachments: MessageAttachmentBase[]; collapsed?: boolean; + id?: string | undefined; }; -const Attachments = ({ attachments, collapsed }: AttachmentsProps): ReactElement => { +const Attachments = ({ attachments, collapsed, id }: AttachmentsProps): ReactElement => { return ( <> {attachments?.map((attachment, index) => ( - + ))} ); diff --git a/apps/meteor/client/components/message/content/attachments/AttachmentsItem.tsx b/apps/meteor/client/components/message/content/attachments/AttachmentsItem.tsx index 6ca97fe7ecde..589549d4bcc1 100644 --- a/apps/meteor/client/components/message/content/attachments/AttachmentsItem.tsx +++ b/apps/meteor/client/components/message/content/attachments/AttachmentsItem.tsx @@ -9,11 +9,12 @@ import { QuoteAttachment } from './QuoteAttachment'; type AttachmentsItemProps = { attachment: MessageAttachmentBase; + id: string | undefined; }; -const AttachmentsItem = ({ attachment }: AttachmentsItemProps): ReactElement => { +const AttachmentsItem = ({ attachment, id }: AttachmentsItemProps): ReactElement => { if (isFileAttachment(attachment)) { - return ; + return ; } if (isQuoteAttachment(attachment)) { diff --git a/apps/meteor/client/components/message/content/attachments/FileAttachment.tsx b/apps/meteor/client/components/message/content/attachments/FileAttachment.tsx index 423464b72569..942ace9055d3 100644 --- a/apps/meteor/client/components/message/content/attachments/FileAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/FileAttachment.tsx @@ -8,7 +8,7 @@ import { GenericFileAttachment } from './file/GenericFileAttachment'; import { ImageAttachment } from './file/ImageAttachment'; import { VideoAttachment } from './file/VideoAttachment'; -export const FileAttachment: FC = (attachment) => { +export const FileAttachment: FC = (attachment) => { if (isFileImageAttachment(attachment)) { return ; } diff --git a/apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx b/apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx index 70c29fafacb3..13cd375c7eeb 100644 --- a/apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx @@ -9,7 +9,8 @@ import MessageContentBody from '../../../MessageContentBody'; import AttachmentImage from '../structure/AttachmentImage'; import { useLoadImage } from './hooks/useLoadImage'; -export const ImageAttachment: FC = ({ +export const ImageAttachment: FC = ({ + id, title, image_url: url, image_preview: imagePreview, @@ -38,6 +39,7 @@ export const ImageAttachment: FC = ({ dataSrc={getURL(link || url)} src={getURL(url)} previewUrl={`data:image/png;base64,${imagePreview}`} + id={id} /> diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx index 00249bb857fc..a82e3a7daa5e 100644 --- a/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx @@ -14,6 +14,7 @@ type AttachmentImageProps = { src: string; loadImage?: boolean; setLoadImage: () => void; + id: string | undefined; } & Dimensions & ({ loadImage: true } | { loadImage: false; setLoadImage: () => void }); @@ -36,7 +37,7 @@ const getDimensions = ( return { width, height, ratio: (height / width) * 100 }; }; -const AttachmentImage: FC = ({ previewUrl, dataSrc, loadImage = true, setLoadImage, src, ...size }) => { +const AttachmentImage: FC = ({ id, previewUrl, dataSrc, loadImage = true, setLoadImage, src, ...size }) => { const limits = useAttachmentDimensions(); const [error, setError] = useState(false); @@ -77,7 +78,14 @@ const AttachmentImage: FC = ({ previewUrl, dataSrc, loadIm right: 0, }} > - + diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index b22627bea8d2..7f4cf6694b7a 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -64,7 +64,7 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM )} - {!!normalizedMessage?.attachments?.length && } + {!!normalizedMessage?.attachments?.length && } {oembedEnabled && !!normalizedMessage.urls?.length && } diff --git a/apps/meteor/client/contexts/ImageGalleryContext.ts b/apps/meteor/client/contexts/ImageGalleryContext.ts new file mode 100644 index 000000000000..2fea5e46a40f --- /dev/null +++ b/apps/meteor/client/contexts/ImageGalleryContext.ts @@ -0,0 +1,13 @@ +import { createContext } from 'react'; + +export type ImageGalleryContextValue = { + imageId: string; + isOpen: boolean; + onClose: () => void; +}; + +export const ImageGalleryContext = createContext({ + imageId: '', + isOpen: false, + onClose: () => undefined, +}); diff --git a/apps/meteor/client/importPackages.ts b/apps/meteor/client/importPackages.ts index 60446388ed9f..ec10c6dd014c 100644 --- a/apps/meteor/client/importPackages.ts +++ b/apps/meteor/client/importPackages.ts @@ -38,7 +38,6 @@ import '../app/slashcommands-open/client'; import '../app/slashcommands-topic/client'; import '../app/slashcommands-unarchiveroom/client'; import '../app/tokenpass/client'; -import '../app/ui/client'; import '../app/webdav/client'; import '../app/webrtc/client'; import '../app/wordpress/client'; diff --git a/apps/meteor/client/lib/lists/ImagesList.ts b/apps/meteor/client/lib/lists/ImagesList.ts new file mode 100644 index 000000000000..540d5bb1c0f7 --- /dev/null +++ b/apps/meteor/client/lib/lists/ImagesList.ts @@ -0,0 +1,39 @@ +import type { IUpload } from '@rocket.chat/core-typings'; + +import { RecordList } from './RecordList'; + +type FilesMessage = Omit & Required>; + +export type ImagesListOptions = { + roomId: Required['rid']; + startingFromId: string; + count?: number; + offset?: number; +}; + +const isFileMessageInRoom = (upload: IUpload, rid: IUpload['rid']): upload is FilesMessage => upload.rid === rid && 'rid' in upload; + +export class ImagesList extends RecordList { + public constructor(private _options: ImagesListOptions) { + super(); + } + + public get options(): ImagesListOptions { + return this._options; + } + + public updateFilters(options: ImagesListOptions): void { + this._options = options; + this.clear(); + } + + protected filter(message: IUpload): boolean { + const { roomId } = this._options; + + if (!isFileMessageInRoom(message, roomId)) { + return false; + } + + return true; + } +} diff --git a/apps/meteor/client/providers/ImageGalleryProvider.tsx b/apps/meteor/client/providers/ImageGalleryProvider.tsx new file mode 100644 index 000000000000..6d14e28c53ce --- /dev/null +++ b/apps/meteor/client/providers/ImageGalleryProvider.tsx @@ -0,0 +1,44 @@ +import React, { type ReactNode, useEffect, useState } from 'react'; + +import ImageGallery from '../components/ImageGallery/ImageGallery'; +import { ImageGalleryContext } from '../contexts/ImageGalleryContext'; + +type ImageGalleryProviderProps = { + children: ReactNode; +}; + +const ImageGalleryProvider = ({ children }: ImageGalleryProviderProps) => { + const [imageId, setImageId] = useState(); + + useEffect(() => { + document.addEventListener('click', (event: Event) => { + const target = event?.target as HTMLElement | null; + if (target?.classList.contains('gallery-item')) { + return setImageId(target.dataset.id || target?.parentElement?.parentElement?.dataset.id); + } + + if (target?.classList.contains('gallery-item-container')) { + return setImageId(target.dataset.id); + } + if ( + target?.classList.contains('gallery-item') && + target?.parentElement?.parentElement?.classList.contains('gallery-item-container') + ) { + return setImageId(target.dataset.id || target?.parentElement?.parentElement?.dataset.id); + } + + if (target?.classList.contains('rcx-avatar__element') && target?.parentElement?.classList.contains('gallery-item')) { + return setImageId(target.dataset.id || target?.parentElement?.parentElement?.dataset.id); + } + }); + }, []); + + return ( + setImageId(undefined) }}> + {children} + {!!imageId && } + + ); +}; + +export default ImageGalleryProvider; diff --git a/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx b/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx index 09caf368b793..5d8ecfed60a9 100644 --- a/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx +++ b/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx @@ -77,7 +77,7 @@ const ContextMessage = ({ )} {message.blocks && } - {message.attachments && } + {message.attachments && } diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.js b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.js index a8ae3e8e26a8..30cbce6a14e1 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.js +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.js @@ -1,9 +1,10 @@ import { css } from '@rocket.chat/css-in-js'; -import { Box, Avatar, Palette } from '@rocket.chat/fuselage'; +import { Box, Palette } from '@rocket.chat/fuselage'; import React from 'react'; import { useFormatDateAndTime } from '../../../../../hooks/useFormatDateAndTime'; import FileItemIcon from './FileItemIcon'; +import ImageItem from './ImageItem'; import MenuItem from './MenuItem'; const hoverClass = css` @@ -20,32 +21,35 @@ const FileItem = ({ fileData, isDeletionAllowed, onClickDelete }) => { return ( - - {typeGroup === 'image' ? : } - - - {name} - - - @{user?.username} - - - {format(uploadedAt)} + {typeGroup === 'image' ? ( + + ) : ( + + + + + {name} + + + @{user?.username} + + + {format(uploadedAt)} + - + )} { + return ( + + {url && } + + {name && ( + + {name} + + )} + {username && ( + + @{username} + + )} + + {timestamp} + + + + ); +}; + +export default ImageItem; diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts b/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts index 8119606e7948..7875b97576f5 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts @@ -19,7 +19,7 @@ export const useFilesList = ( } => { const [filesList, setFilesList] = useState(() => new FilesList(options)); const reload = useCallback(() => setFilesList(new FilesList(options)), [options]); - const room = useUserRoom(options.rid as string); + const room = useUserRoom(options.rid); const uid = useUserId(); useComponentDidUpdate(() => { diff --git a/apps/meteor/client/views/room/providers/RoomProvider.tsx b/apps/meteor/client/views/room/providers/RoomProvider.tsx index 82c66c6f5d8d..47d65562460a 100644 --- a/apps/meteor/client/views/room/providers/RoomProvider.tsx +++ b/apps/meteor/client/views/room/providers/RoomProvider.tsx @@ -10,6 +10,7 @@ import { useReactiveQuery } from '../../../hooks/useReactiveQuery'; import { useReactiveValue } from '../../../hooks/useReactiveValue'; import { RoomManager } from '../../../lib/RoomManager'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; +import ImageGalleryProvider from '../../../providers/ImageGalleryProvider'; import RoomNotFound from '../RoomNotFound'; import RoomSkeleton from '../RoomSkeleton'; import { useRoomRolesManagement } from '../body/hooks/useRoomRolesManagement'; @@ -109,7 +110,9 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { return ( - {children} + + {children} + ); diff --git a/apps/meteor/package.json b/apps/meteor/package.json index bf632810853b..fb5204d1ed4b 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -135,7 +135,6 @@ "@types/nodemailer": "^6.4.13", "@types/oauth2-server": "^3.0.15", "@types/parseurl": "^1.3.2", - "@types/photoswipe": "^4.1.5", "@types/prometheus-gc-stats": "^0.6.3", "@types/proxyquire": "^1.3.30", "@types/psl": "^1.1.2", @@ -351,7 +350,6 @@ "ip-range-check": "^0.2.0", "is-svg": "^4.3.2", "isolated-vm": "4.4.2", - "jquery": "^3.6.0", "jschardet": "^3.0.0", "jsdom": "^16.7.0", "jsrsasign": "^10.5.24", @@ -388,7 +386,6 @@ "path": "^0.12.7", "path-to-regexp": "^6.2.1", "pdfjs-dist": "^2.13.216", - "photoswipe": "^4.1.3", "pino": "^8.15.0", "postis": "^2.2.0", "prom-client": "^14.0.1", @@ -418,6 +415,7 @@ "strict-uri-encode": "^2.0.0", "string-strip-html": "^7.0.3", "suretype": "~2.4.1", + "swiper": "^9.3.2", "tar-stream": "^1.6.2", "textarea-caret": "^3.1.0", "tinykeys": "^1.4.0", diff --git a/apps/meteor/server/models/raw/Uploads.ts b/apps/meteor/server/models/raw/Uploads.ts index 1c02fffab2a8..f2dd26113eaa 100644 --- a/apps/meteor/server/models/raw/Uploads.ts +++ b/apps/meteor/server/models/raw/Uploads.ts @@ -1,5 +1,5 @@ // TODO: Lib imports should not exists inside the raw models -import type { IUpload, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; +import type { IUpload, RocketChatRecordDeleted, IRoom } from '@rocket.chat/core-typings'; import type { FindPaginated, IUploadsModel } from '@rocket.chat/model-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import type { Collection, FindCursor, Db, IndexDescription, WithId, Filter, FindOptions } from 'mongodb'; @@ -57,4 +57,27 @@ export class UploadsRaw extends BaseUploadModelRaw implements IUploadsModel { options, ); } + + findImagesByRoomId( + rid: IRoom['_id'], + uploadedAt?: Date, + options: Omit, 'sort'> = {}, + ): FindPaginated>> { + return this.findPaginated( + { + rid, + _hidden: { $ne: true }, + typeGroup: 'image', + ...(Boolean(uploadedAt) && { + uploadedAt: { + $lte: uploadedAt, + }, + }), + }, + { + ...options, + sort: { uploadedAt: -1 }, + }, + ); + } } diff --git a/packages/model-typings/src/models/IUploadsModel.ts b/packages/model-typings/src/models/IUploadsModel.ts index d5be641c9c37..1e80fcfe39b5 100644 --- a/packages/model-typings/src/models/IUploadsModel.ts +++ b/packages/model-typings/src/models/IUploadsModel.ts @@ -1,5 +1,5 @@ -import type { IUpload } from '@rocket.chat/core-typings'; -import type { FindCursor, WithId, Filter } from 'mongodb'; +import type { IRoom, IUpload } from '@rocket.chat/core-typings'; +import type { FindCursor, WithId, Filter, FindOptions } from 'mongodb'; import type { FindPaginated } from './IBaseModel'; import type { IBaseUploadsModel } from './IBaseUploadsModel'; @@ -8,4 +8,10 @@ export interface IUploadsModel extends IBaseUploadsModel { findNotHiddenFilesOfRoom(roomId: string, searchText: string, fileType: string, limit: number): FindCursor; findPaginatedWithoutThumbs(query: Filter, options?: any): FindPaginated>>; + + findImagesByRoomId( + rid: IRoom['_id'], + uploadedAt?: Date, + options?: Omit, 'sort'>, + ): FindPaginated>>; } diff --git a/packages/rest-typings/src/v1/channels/ChannelsImagesProps.ts b/packages/rest-typings/src/v1/channels/ChannelsImagesProps.ts new file mode 100644 index 000000000000..52c065b7c393 --- /dev/null +++ b/packages/rest-typings/src/v1/channels/ChannelsImagesProps.ts @@ -0,0 +1,14 @@ +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +export type ChannelsImagesProps = { + roomId: string; + startingFromId: string; + count?: number; + offset?: number; +}; +const channelsImagesPropsSchema = {}; +export const isChannelsImagesProps = ajv.compile(channelsImagesPropsSchema); diff --git a/packages/rest-typings/src/v1/channels/channels.ts b/packages/rest-typings/src/v1/channels/channels.ts index e5ae6175ffdf..eccdf3499847 100644 --- a/packages/rest-typings/src/v1/channels/channels.ts +++ b/packages/rest-typings/src/v1/channels/channels.ts @@ -10,6 +10,7 @@ import type { ChannelsDeleteProps } from './ChannelsDeleteProps'; import type { ChannelsGetAllUserMentionsByChannelProps } from './ChannelsGetAllUserMentionsByChannelProps'; import type { ChannelsGetIntegrationsProps } from './ChannelsGetIntegrationsProps'; import type { ChannelsHistoryProps } from './ChannelsHistoryProps'; +import type { ChannelsImagesProps } from './ChannelsImagesProps'; import type { ChannelsInviteProps } from './ChannelsInviteProps'; import type { ChannelsJoinProps } from './ChannelsJoinProps'; import type { ChannelsKickProps } from './ChannelsKickProps'; @@ -38,6 +39,11 @@ export type ChannelsEndpoints = { files: IUpload[]; }>; }; + '/v1/channels.images': { + GET: (params: ChannelsImagesProps) => PaginatedResult<{ + files: IUpload[]; + }>; + }; '/v1/channels.members': { GET: ( params: PaginatedRequest< diff --git a/packages/rest-typings/src/v1/channels/index.ts b/packages/rest-typings/src/v1/channels/index.ts index 981296e244fe..f0cf81ba622d 100644 --- a/packages/rest-typings/src/v1/channels/index.ts +++ b/packages/rest-typings/src/v1/channels/index.ts @@ -7,6 +7,7 @@ export * from './ChannelsCreateProps'; export * from './ChannelsDeleteProps'; export * from './ChannelsGetAllUserMentionsByChannelProps'; export * from './ChannelsHistoryProps'; +export * from './ChannelsImagesProps'; export * from './ChannelsJoinProps'; export * from './ChannelsKickProps'; export * from './ChannelsLeaveProps'; diff --git a/yarn.lock b/yarn.lock index 1aeb50860298..cd3958856cfc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8217,9 +8217,9 @@ __metadata: "@rocket.chat/icons": "*" "@rocket.chat/prettier-config": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-contexts": 3.0.0-rc.14 + "@rocket.chat/ui-contexts": 3.0.0 "@rocket.chat/ui-kit": "*" - "@rocket.chat/ui-video-conf": 3.0.0-rc.14 + "@rocket.chat/ui-video-conf": 3.0.0 "@tanstack/react-query": "*" react: "*" react-dom: "*" @@ -8307,8 +8307,8 @@ __metadata: "@rocket.chat/fuselage-tokens": "*" "@rocket.chat/message-parser": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-client": 3.0.0-rc.14 - "@rocket.chat/ui-contexts": 3.0.0-rc.14 + "@rocket.chat/ui-client": 3.0.0 + "@rocket.chat/ui-contexts": 3.0.0 katex: "*" react: "*" languageName: unknown @@ -8697,7 +8697,6 @@ __metadata: "@types/oauth2-server": ^3.0.15 "@types/object-path": ^0.11.3 "@types/parseurl": ^1.3.2 - "@types/photoswipe": ^4.1.5 "@types/prometheus-gc-stats": ^0.6.3 "@types/proxy-from-env": ^1.0.3 "@types/proxyquire": ^1.3.30 @@ -8811,7 +8810,6 @@ __metadata: is-svg: ^4.3.2 isolated-vm: 4.4.2 jest: ~29.6.4 - jquery: ^3.6.0 jschardet: ^3.0.0 jsdom: ^16.7.0 jsdom-global: ^3.0.2 @@ -8852,7 +8850,6 @@ __metadata: path: ^0.12.7 path-to-regexp: ^6.2.1 pdfjs-dist: ^2.13.216 - photoswipe: ^4.1.3 pino: ^8.15.0 pino-pretty: ^7.6.1 playwright-qase-reporter: ^1.2.1 @@ -8900,6 +8897,7 @@ __metadata: stylelint-order: ^5.0.0 supertest: ^6.2.3 suretype: ~2.4.1 + swiper: ^9.3.2 tar-stream: ^1.6.2 template-file: ^6.0.1 textarea-caret: ^3.1.0 @@ -9470,7 +9468,7 @@ __metadata: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" - "@rocket.chat/ui-contexts": 3.0.0-rc.14 + "@rocket.chat/ui-contexts": 3.0.0 react: ~17.0.2 languageName: unknown linkType: soft @@ -9623,7 +9621,7 @@ __metadata: "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-contexts": 3.0.0-rc.14 + "@rocket.chat/ui-contexts": 3.0.0 react: ^17.0.2 react-dom: ^17.0.2 languageName: unknown @@ -9709,7 +9707,7 @@ __metadata: peerDependencies: "@rocket.chat/layout": "*" "@rocket.chat/tools": "*" - "@rocket.chat/ui-contexts": 3.0.0-rc.14 + "@rocket.chat/ui-contexts": 3.0.0 "@tanstack/react-query": "*" react: "*" react-hook-form: "*" @@ -12862,13 +12860,6 @@ __metadata: languageName: node linkType: hard -"@types/photoswipe@npm:^4.1.5": - version: 4.1.5 - resolution: "@types/photoswipe@npm:4.1.5" - checksum: a6b5fd6b029b8e84bc43f680549abf33dbc02e551e8dd176e29ca82e80edf66d0b62777fd42fe415d9e52b7a570718cac684aaea544f8dc084d1cb8bcc422f0f - languageName: node - linkType: hard - "@types/polka@npm:^0.5.6": version: 0.5.6 resolution: "@types/polka@npm:0.5.6" @@ -26297,13 +26288,6 @@ __metadata: languageName: node linkType: hard -"jquery@npm:^3.6.0": - version: 3.6.0 - resolution: "jquery@npm:3.6.0" - checksum: 8fd5fef4aa48fd374ec716dd1c1df1af407814a228e15c1260ca140de3a697c2a77c30c54ff1d238b6a3ab4ddc445ddeef9adce6c6d28e4869d85eb9d3951c0e - languageName: node - linkType: hard - "js-git@npm:^0.7.8": version: 0.7.8 resolution: "js-git@npm:0.7.8" @@ -31018,13 +31002,6 @@ __metadata: languageName: node linkType: hard -"photoswipe@npm:^4.1.3": - version: 4.1.3 - resolution: "photoswipe@npm:4.1.3" - checksum: 2eecc188d81642832cd5086e3954a6b5546b70651b6a76752625d9ff7b5e63ede9347316edc8110d7dba90acf55d2e8b2e917f454ed46c2594b09084c383aa37 - languageName: node - linkType: hard - "picocolors@npm:^0.2.1": version: 0.2.1 resolution: "picocolors@npm:0.2.1" @@ -35914,6 +35891,13 @@ __metadata: languageName: node linkType: hard +"ssr-window@npm:^4.0.2": + version: 4.0.2 + resolution: "ssr-window@npm:4.0.2" + checksum: df182600927f4f3225224cf8c02338ea637c9750519505bbfb9a9236741a2a7ec088386fb948bca7b447b8303d9109e7dc7672e3de041c79ac2a0e03665af7d2 + languageName: node + linkType: hard + "ssri@npm:^6.0.1": version: 6.0.2 resolution: "ssri@npm:6.0.2" @@ -36881,6 +36865,15 @@ __metadata: languageName: node linkType: hard +"swiper@npm:^9.3.2": + version: 9.3.2 + resolution: "swiper@npm:9.3.2" + dependencies: + ssr-window: ^4.0.2 + checksum: 9ccb6a0ef67d71ac780d7d1f3b2c2daf6f1846d3df8810aff6dcb8501c87b7769540a869cdd83eb902e5278396e5379cb32d9ed397234f7700ef55f0209f4592 + languageName: node + linkType: hard + "symbol-tree@npm:^3.2.4": version: 3.2.4 resolution: "symbol-tree@npm:3.2.4" From 92ee9fa284e5a243122b1411c4d91daa2a409f8b Mon Sep 17 00:00:00 2001 From: Heitor Tanoue <68477006+heitortanoue@users.noreply.github.com> Date: Mon, 4 Dec 2023 15:11:25 -0300 Subject: [PATCH 2/9] fix: Discussion messages deleted despite the "Do not delete discussion messages" retention policy enabled (#31113) --- .changeset/fifty-ducks-sing.md | 5 +++++ apps/meteor/app/retention-policy/server/cronPruneMessages.ts | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 .changeset/fifty-ducks-sing.md diff --git a/.changeset/fifty-ducks-sing.md b/.changeset/fifty-ducks-sing.md new file mode 100644 index 000000000000..3f8d3bda1aa5 --- /dev/null +++ b/.changeset/fifty-ducks-sing.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: Discussion messages deleted despite the "Do not delete discussion messages" retention policy enabled diff --git a/apps/meteor/app/retention-policy/server/cronPruneMessages.ts b/apps/meteor/app/retention-policy/server/cronPruneMessages.ts index 6ab3ff1de226..fb0e691abd69 100644 --- a/apps/meteor/app/retention-policy/server/cronPruneMessages.ts +++ b/apps/meteor/app/retention-policy/server/cronPruneMessages.ts @@ -24,6 +24,8 @@ async function job(): Promise { const ignoreDiscussion = settings.get('RetentionPolicy_DoNotPruneDiscussion'); const ignoreThreads = settings.get('RetentionPolicy_DoNotPruneThreads'); + const ignoreDiscussionQuery = ignoreDiscussion ? { prid: { $exists: false } } : {}; + // get all rooms with default values for await (const type of types) { const maxAge = maxTimes[type] || 0; @@ -34,6 +36,7 @@ async function job(): Promise { 't': type, '$or': [{ 'retention.enabled': { $eq: true } }, { 'retention.enabled': { $exists: false } }], 'retention.overrideGlobal': { $ne: true }, + ...ignoreDiscussionQuery, }, { projection: { _id: 1 } }, ).toArray(); @@ -56,6 +59,7 @@ async function job(): Promise { 'retention.enabled': { $eq: true }, 'retention.overrideGlobal': { $eq: true }, 'retention.maxAge': { $gte: 0 }, + ...ignoreDiscussionQuery, }, { projection: { _id: 1, retention: 1 } }, ).toArray(); From 58cbc5148cd6117bcfab5c98bc3b125000a3a9e4 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 4 Dec 2023 18:39:52 -0300 Subject: [PATCH 3/9] chore: rocketchat:coverage package (#30339) --- .github/actions/meteor-build/action.yml | 9 +++- .github/workflows/ci-test-e2e.yml | 29 ++++++++++ apps/meteor/package.json | 2 +- .../.npm/package/.gitignore | 1 + .../rocketchat-coverage/.npm/package/README | 7 +++ .../.npm/package/npm-shrinkwrap.json | 52 ++++++++++++++++++ .../packages/rocketchat-coverage/package.js | 18 +++++++ .../plugin/compile-version.js | 54 +++++++++++++++++++ codecov.yml | 2 + docker-compose-ci.yml | 4 ++ 10 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 apps/meteor/packages/rocketchat-coverage/.npm/package/.gitignore create mode 100644 apps/meteor/packages/rocketchat-coverage/.npm/package/README create mode 100644 apps/meteor/packages/rocketchat-coverage/.npm/package/npm-shrinkwrap.json create mode 100644 apps/meteor/packages/rocketchat-coverage/package.js create mode 100644 apps/meteor/packages/rocketchat-coverage/plugin/compile-version.js diff --git a/.github/actions/meteor-build/action.yml b/.github/actions/meteor-build/action.yml index 21fec059c8de..0229e5bb5ba0 100644 --- a/.github/actions/meteor-build/action.yml +++ b/.github/actions/meteor-build/action.yml @@ -109,7 +109,14 @@ runs: env: METEOR_PROFILE: 1000 BABEL_ENV: ${{ inputs.coverage == 'true' && 'coverage' || '' }} - run: yarn build:ci -- --directory /tmp/dist + run: | + # check if BABEL_ENV is set to coverage + if [[ $BABEL_ENV == "coverage" ]]; then + echo -e "rocketchat:coverage\n" >> ./apps/meteor/.meteor/packages + echo "Coverage enabled" + fi + + yarn build:ci -- --directory /tmp/dist - name: Build Rocket.Chat shell: bash diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index e70d4c80217e..eb7b228022c3 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -127,6 +127,12 @@ jobs: - run: yarn build + - name: Prepare code coverage directory + if: inputs.release == 'ee' + run: | + mkdir -p /tmp/coverage + chmod 777 /tmp/coverage + - name: Start containers for CE if: inputs.release == 'ce' env: @@ -141,6 +147,8 @@ jobs: MONGO_URL: 'mongodb://host.docker.internal:27017/rocketchat?replicaSet=rs0&directConnection=true' ENTERPRISE_LICENSE: ${{ inputs.enterprise-license }} TRANSPORTER: ${{ inputs.transporter }} + COVERAGE_DIR: '/tmp/coverage' + COVERAGE_REPORTER: 'lcov' run: | docker compose -f docker-compose-ci.yml up -d @@ -184,6 +192,8 @@ jobs: env: WEBHOOK_TEST_URL: 'http://host.docker.internal:10000' IS_EE: ${{ inputs.release == 'ee' && 'true' || '' }} + COVERAGE_DIR: '/tmp/coverage' + COVERAGE_REPORTER: 'lcovonly' run: | for i in $(seq 1 2); do npm run testapi && s=0 && break || s=$? @@ -204,6 +214,9 @@ jobs: sleep 10 done; done; + docker compose -f ../../docker-compose-ci.yml stop + + ls -l $COVERAGE_DIR exit $s - name: E2E Test UI (${{ matrix.shard }}/${{ inputs.total-shard }}) @@ -249,6 +262,22 @@ jobs: verbose: true token: ${{ secrets.CODECOV_TOKEN }} + - uses: codecov/codecov-action@v3 + if: inputs.type == 'api' && inputs.release == 'ee' + with: + directory: /tmp/coverage + working-directory: . + flags: e2e-api + verbose: true + token: ${{ secrets.CODECOV_TOKEN }} + + - name: Store e2e-api-ee-coverage + if: inputs.type == 'api' && inputs.release == 'ee' + uses: actions/upload-artifact@v3 + with: + name: e2e-api-ee-coverage + path: /tmp/coverage + - name: Store e2e-ee-coverage if: inputs.type == 'ui' && inputs.release == 'ee' uses: actions/upload-artifact@v3 diff --git a/apps/meteor/package.json b/apps/meteor/package.json index fb5204d1ed4b..eaaec7de67f3 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -35,7 +35,7 @@ "coverage": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' nyc -r html mocha --config ./.mocharc.js", "test:e2e": "playwright test", "test:e2e:federation": "playwright test --config=playwright-federation.config.ts", - "test:e2e:nyc": "nyc report --reporter=text-summary --reporter=lcov", + "test:e2e:nyc": "nyc report --reporter=lcov", "testapi": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' mocha --config ./.mocharc.api.js", "testunit": "npm run .testunit:definition && npm run .testunit:client && npm run .testunit:server:cov", ".testunit:server": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' mocha --config ./.mocharc.js", diff --git a/apps/meteor/packages/rocketchat-coverage/.npm/package/.gitignore b/apps/meteor/packages/rocketchat-coverage/.npm/package/.gitignore new file mode 100644 index 000000000000..3c3629e647f5 --- /dev/null +++ b/apps/meteor/packages/rocketchat-coverage/.npm/package/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/apps/meteor/packages/rocketchat-coverage/.npm/package/README b/apps/meteor/packages/rocketchat-coverage/.npm/package/README new file mode 100644 index 000000000000..3d492553a438 --- /dev/null +++ b/apps/meteor/packages/rocketchat-coverage/.npm/package/README @@ -0,0 +1,7 @@ +This directory and the files immediately inside it are automatically generated +when you change this package's NPM dependencies. Commit the files in this +directory (npm-shrinkwrap.json, .gitignore, and this README) to source control +so that others run the same versions of sub-dependencies. + +You should NOT check in the node_modules directory that Meteor automatically +creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/apps/meteor/packages/rocketchat-coverage/.npm/package/npm-shrinkwrap.json b/apps/meteor/packages/rocketchat-coverage/.npm/package/npm-shrinkwrap.json new file mode 100644 index 000000000000..526c4cbd2d5c --- /dev/null +++ b/apps/meteor/packages/rocketchat-coverage/.npm/package/npm-shrinkwrap.json @@ -0,0 +1,52 @@ +{ + "lockfileVersion": 1, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==" + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dependencies": { + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==" + } + } + }, + "istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==" + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==" + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==" + } + } +} diff --git a/apps/meteor/packages/rocketchat-coverage/package.js b/apps/meteor/packages/rocketchat-coverage/package.js new file mode 100644 index 000000000000..55dde32fec3d --- /dev/null +++ b/apps/meteor/packages/rocketchat-coverage/package.js @@ -0,0 +1,18 @@ +Package.describe({ + name: 'rocketchat:coverage', + summary: '', + version: '1.0.0', +}); + +Package.onUse(function (api) { + api.use('ecmascript'); + api.use('isobuild:compiler-plugin@1.0.0'); + + api.mainModule('plugin/compile-version.js', 'server'); +}); + +Npm.depends({ + 'istanbul-lib-report': '3.0.0', + 'istanbul-reports': '3.0.2', + 'istanbul-lib-coverage': '3.0.0', +}); diff --git a/apps/meteor/packages/rocketchat-coverage/plugin/compile-version.js b/apps/meteor/packages/rocketchat-coverage/plugin/compile-version.js new file mode 100644 index 000000000000..869fbcbf15d3 --- /dev/null +++ b/apps/meteor/packages/rocketchat-coverage/plugin/compile-version.js @@ -0,0 +1,54 @@ +import { exec } from 'child_process'; +import os from 'os'; +import util from 'util'; + +import libReport from 'istanbul-lib-report'; +import reports from 'istanbul-reports'; +import libCoverage from 'istanbul-lib-coverage'; + +const dir = process.env.COVERAGE_DIR; +const reporter = process.env.COVERAGE_REPORTER || 'lcov'; + +console.log('Coverage plugin started'); + +if (!dir && !reporter) { + return console.log('Coverage plugin not configured'); +} + +if (!dir || !reporter) { + console.log('Coverage plugin not fully configured'); + return; +} + +process.on('exit', async () => { + try { + if (!dir) { + throw new Error('No coverage dir'); + } + + if (!reporter) { + throw new Error('No coverage reporter'); + } + console.log('Coverage plugin triggered'); + + const coverageMap = libCoverage.createCoverageMap(globalThis['__coverage__']); + + const configWatermarks = { + statements: [50, 80], + functions: [50, 80], + branches: [50, 80], + lines: [50, 80], + }; + + const context = libReport.createContext({ + dir, + coverageMap, + }); + + const report = reports.create(reporter); + + report.execute(context); + } catch (e) { + console.log('Error', e); + } +}); diff --git a/codecov.yml b/codecov.yml index 2fe2eaf32b42..2a163eae4d74 100644 --- a/codecov.yml +++ b/codecov.yml @@ -26,3 +26,5 @@ flags: comment: layout: 'reach, diff, flags' +fixes: + - '/home/runner/work/Rocket.Chat/Rocket.Chat/::' diff --git a/docker-compose-ci.yml b/docker-compose-ci.yml index e859772f007b..f3ce12026d49 100644 --- a/docker-compose-ci.yml +++ b/docker-compose-ci.yml @@ -2,6 +2,8 @@ version: '3.8' services: rocketchat: + volumes: + - /tmp/coverage:/tmp/coverage platform: linux/amd64 build: dockerfile: ${RC_DOCKERFILE} @@ -15,6 +17,8 @@ services: - 'TRANSPORTER=${TRANSPORTER}' - MOLECULER_LOG_LEVEL=info - 'ROCKETCHAT_LICENSE=${ENTERPRISE_LICENSE}' + - 'COVERAGE_DIR=${COVERAGE_DIR}' + - 'COVERAGE_REPORTER=${COVERAGE_REPORTER}' extra_hosts: - 'host.docker.internal:host-gateway' depends_on: From a7bee82a1b4474f26030fed4f338f7ae4a51d6d7 Mon Sep 17 00:00:00 2001 From: Hardik Bhatia <98163873+hardikbhatia777@users.noreply.github.com> Date: Tue, 5 Dec 2023 18:13:40 +0530 Subject: [PATCH 4/9] i18n: Fixed all German translation handlebar errors (#31139) --- apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json index 9faa71e252d0..6601535bc40b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json @@ -3208,7 +3208,7 @@ "Message_has_been_edited": "Die Nachricht wurde bearbeitet", "Message_has_been_edited_at": "Die Nachricht wurde am {{date}} bearbeitet", "Message_has_been_edited_by": "Die Nachricht wurde editiert von {{username}}", - "Message_has_been_edited_by_at": "Die Nachricht wurde bearbeitet von __Benutzername__ am __datum__", + "Message_has_been_edited_by_at": "Die Nachricht wurde bearbeitet von {{username}} am {{date}}", "Message_has_been_pinned": "Nachricht wurde angeheftet", "Message_has_been_starred": "Nachricht wurde als Favorit gekennzeichnet", "Message_has_been_unpinned": "Nachricht wurde entpinnt", @@ -3332,7 +3332,7 @@ "Moderation_Delete_message": "Nachricht löschen", "Moderation_Dismiss_and_delete": "Ablehnen und löschen", "Moderation_Delete_this_message": "Diese Nachricht löschen", - "Moderation_Message_context_header": "Nachricht(en) von __displayName__", + "Moderation_Message_context_header": "Nachricht(s) gemeldet", "Moderation_Action_View_reports": "Gemeldete Nachrichten anzeigen", "Moderation_Deactivate_User": "Nutzer deaktivieren", "Moderation_Delete_all_messages": "Alle Nachrichten löschen", @@ -4999,7 +4999,7 @@ "User_Settings": "Kontoeinstellungen", "User_started_a_new_conversation": "{{username}} hat ein neues Gespräch begonnen", "User_unmuted_by": "Benutzer {{user_unmuted}} wurde das Chatten von {{user_by}} wieder erlaubt", - "User_has_been_unmuted": "__user_muted__ nicht mehr stummgeschaltet", + "User_has_been_unmuted": "{{user_unmuted}} nicht mehr stummgeschaltet", "User_unmuted_in_room": "Dem Benutzer wurde das Chatten wieder erlaubt", "User_updated_successfully": "Benutzer wurde erfolgreich aktualisiert", "User_uploaded_a_file_on_channel": "{{username}} hat eine Datei in {{channel}} hochgeladen", From 200b1fea6370e69ba9e589ff92d73e7111d3e9ef Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Tue, 5 Dec 2023 12:52:29 -0300 Subject: [PATCH 5/9] fix: New `custom-roles` license module isn't properly checked (#31153) --- .changeset/fresh-radios-whisper.md | 5 +++++ .../client/views/admin/permissions/EditRolePage.tsx | 2 +- .../views/admin/permissions/EditRolePageWithData.tsx | 8 ++++---- .../views/admin/permissions/PermissionsContextBar.tsx | 9 ++++----- apps/meteor/ee/server/api/roles.ts | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json | 2 +- 6 files changed, 17 insertions(+), 13 deletions(-) create mode 100644 .changeset/fresh-radios-whisper.md diff --git a/.changeset/fresh-radios-whisper.md b/.changeset/fresh-radios-whisper.md new file mode 100644 index 000000000000..cba234524dce --- /dev/null +++ b/.changeset/fresh-radios-whisper.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed issue with the new `custom-roles` license module not being checked throughout the application diff --git a/apps/meteor/client/views/admin/permissions/EditRolePage.tsx b/apps/meteor/client/views/admin/permissions/EditRolePage.tsx index b7948d571254..8980d64d17e8 100644 --- a/apps/meteor/client/views/admin/permissions/EditRolePage.tsx +++ b/apps/meteor/client/views/admin/permissions/EditRolePage.tsx @@ -74,7 +74,7 @@ const EditRolePage = ({ role, isEnterprise }: { role?: IRole; isEnterprise: bool } }; - const deleteRoleMessage = isEnterprise ? t('Delete_Role_Warning') : t('Delete_Role_Warning_Community_Edition'); + const deleteRoleMessage = isEnterprise ? t('Delete_Role_Warning') : t('Delete_Role_Warning_Not_Enterprise'); setModal( {t('error-invalid-role')}; } - if (isLoading || !data) { + if (hasCustomRolesModule === 'loading') { return ; } - return ; + return ; }; export default EditRolePageWithData; diff --git a/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx b/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx index b472e57d7eae..767593067547 100644 --- a/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx +++ b/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx @@ -3,8 +3,8 @@ import { useRouteParameter, useRoute, useTranslation, useSetModal } from '@rocke import type { ReactElement } from 'react'; import React, { useEffect } from 'react'; +import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule'; import { Contextualbar, ContextualbarHeader, ContextualbarTitle, ContextualbarClose } from '../../../components/Contextualbar'; -import { useIsEnterprise } from '../../../hooks/useIsEnterprise'; import CustomRoleUpsellModal from './CustomRoleUpsellModal'; import EditRolePageWithData from './EditRolePageWithData'; @@ -14,21 +14,20 @@ const PermissionsContextBar = (): ReactElement | null => { const context = useRouteParameter('context'); const router = useRoute('admin-permissions'); const setModal = useSetModal(); - const { data } = useIsEnterprise(); - const isEnterprise = !!data?.isEnterprise; + const hasCustomRolesModule = useHasLicenseModule('custom-roles') === true; const handleCloseContextualbar = useMutableCallback(() => { router.push({}); }); useEffect(() => { - if (context !== 'new' || isEnterprise) { + if (context !== 'new' || hasCustomRolesModule) { return; } setModal( setModal(null)} />); handleCloseContextualbar(); - }, [context, isEnterprise, handleCloseContextualbar, setModal]); + }, [context, hasCustomRolesModule, handleCloseContextualbar, setModal]); return ( (context && ( diff --git a/apps/meteor/ee/server/api/roles.ts b/apps/meteor/ee/server/api/roles.ts index c10c32c3ee1a..7a71613b3548 100644 --- a/apps/meteor/ee/server/api/roles.ts +++ b/apps/meteor/ee/server/api/roles.ts @@ -96,7 +96,7 @@ API.v1.addRoute( { authRequired: true }, { async post() { - if (!License.hasValidLicense()) { + if (!License.hasModule('custom-roles')) { throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature'); } @@ -154,7 +154,7 @@ API.v1.addRoute( const role = await Roles.findOne(roleId); - if (!License.hasValidLicense() && !role?.protected) { + if (!License.hasModule('custom-roles') && !role?.protected) { throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature'); } diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 62b87f4a75d6..6898eb26e4a1 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1561,7 +1561,7 @@ "Delete_message": "Delete message", "Delete_my_account": "Delete my account", "Delete_Role_Warning": "This cannot be undone", - "Delete_Role_Warning_Community_Edition": "This cannot be undone. Note that it's not possible to create new custom roles in a Community workspace", + "Delete_Role_Warning_Not_Enterprise": "This cannot be undone. You won't be able to create a new custom role, since that feature is no longer available for your current plan.", "Delete_Room_Warning": "Deleting a room will delete all messages posted within the room. This cannot be undone.", "Delete_User_Warning": "Deleting a user will delete all messages from that user as well. This cannot be undone.", "Delete_User_Warning_Delete": "Deleting a user will delete all messages from that user as well. This cannot be undone.", From e6323bedb5f1895c306f9b1a4a5b49927fbe4227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:24:11 -0300 Subject: [PATCH 6/9] chore: ImageGallery tweaks (#31159) --- .../components/ImageGallery/ImageGallery.tsx | 15 +++++++++++++-- .../ImageGallery/ImageGalleryLoader.tsx | 4 ++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx index 2946e3e692a2..0675532432aa 100644 --- a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx +++ b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx @@ -1,5 +1,5 @@ import { css } from '@rocket.chat/css-in-js'; -import { Box, IconButton, Throbber } from '@rocket.chat/fuselage'; +import { Box, IconButton, Palette, Throbber } from '@rocket.chat/fuselage'; import React, { useRef, useState } from 'react'; import { FocusScope } from 'react-aria'; import { createPortal } from 'react-dom'; @@ -83,6 +83,17 @@ const swiperStyle = css` right: 10px; left: auto; } + + .rcx-lazy-preloader { + position: absolute; + z-index: -1; + left: 50%; + top: 50%; + + transform: translate(-50%, -50%); + + color: ${Palette.text['font-pure-white']}; + } `; const ImageGallery = () => { @@ -122,7 +133,7 @@ const ImageGallery = () => {
- +
diff --git a/apps/meteor/client/components/ImageGallery/ImageGalleryLoader.tsx b/apps/meteor/client/components/ImageGallery/ImageGalleryLoader.tsx index a495f345194e..131b82780f22 100644 --- a/apps/meteor/client/components/ImageGallery/ImageGalleryLoader.tsx +++ b/apps/meteor/client/components/ImageGallery/ImageGalleryLoader.tsx @@ -12,9 +12,9 @@ const closeButtonStyle = css` const ImageGalleryLoader = ({ onClose }: { onClose: () => void }) => createPortal( - + - + , document.body, ); From 0681c455fc95a9584d446b0ac50580a21735c706 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Tue, 5 Dec 2023 16:39:37 -0300 Subject: [PATCH 7/9] chore: Replace `useForm` in favor of RHF on Omnichannel `AgentsEdit` (#30789) Co-authored-by: Martin Schoeler --- .../views/omnichannel/additionalForms.tsx | 4 +- .../views/omnichannel/agents/AgentEdit.tsx | 341 ++++++++++-------- .../omnichannel/agents/AgentEditWithData.tsx | 40 +- .../views/omnichannel/agents/AgentInfo.tsx | 100 +++-- .../omnichannel/agents/AgentInfoActions.tsx | 56 --- .../views/omnichannel/agents/AgentsPage.tsx | 15 +- .../views/omnichannel/agents/AgentsTab.tsx | 44 --- .../agents/AgentsTable/AgentsTable.tsx | 14 +- .../agents/AgentsTable/AgentsTableRow.tsx | 44 +-- .../agents/AgentsTable/RemoveAgentButton.tsx | 53 --- .../agents/hooks/useRemoveAgent.tsx | 36 ++ .../additionalForms/MaxChatsPerAgent.tsx | 31 +- .../MaxChatsPerAgentContainer.js | 23 -- ...Display.js => MaxChatsPerAgentDisplay.tsx} | 6 +- packages/core-typings/src/ILivechatAgent.ts | 3 +- 15 files changed, 359 insertions(+), 451 deletions(-) delete mode 100644 apps/meteor/client/views/omnichannel/agents/AgentInfoActions.tsx delete mode 100644 apps/meteor/client/views/omnichannel/agents/AgentsTab.tsx delete mode 100644 apps/meteor/client/views/omnichannel/agents/AgentsTable/RemoveAgentButton.tsx create mode 100644 apps/meteor/client/views/omnichannel/agents/hooks/useRemoveAgent.tsx delete mode 100644 apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentContainer.js rename apps/meteor/ee/client/omnichannel/additionalForms/{MaxChatsPerAgentDisplay.js => MaxChatsPerAgentDisplay.tsx} (77%) diff --git a/apps/meteor/client/views/omnichannel/additionalForms.tsx b/apps/meteor/client/views/omnichannel/additionalForms.tsx index 152245df90be..16f6c9bcb440 100644 --- a/apps/meteor/client/views/omnichannel/additionalForms.tsx +++ b/apps/meteor/client/views/omnichannel/additionalForms.tsx @@ -7,14 +7,14 @@ import DepartmentForwarding from '../../../ee/client/omnichannel/additionalForms import EeNumberInput from '../../../ee/client/omnichannel/additionalForms/EeNumberInput'; import EeTextAreaInput from '../../../ee/client/omnichannel/additionalForms/EeTextAreaInput'; import EeTextInput from '../../../ee/client/omnichannel/additionalForms/EeTextInput'; -import MaxChatsPerAgentContainer from '../../../ee/client/omnichannel/additionalForms/MaxChatsPerAgentContainer'; +import MaxChatsPerAgent from '../../../ee/client/omnichannel/additionalForms/MaxChatsPerAgent'; import MaxChatsPerAgentDisplay from '../../../ee/client/omnichannel/additionalForms/MaxChatsPerAgentDisplay'; import PrioritiesSelect from '../../../ee/client/omnichannel/additionalForms/PrioritiesSelect'; import SlaPoliciesSelect from '../../../ee/client/omnichannel/additionalForms/SlaPoliciesSelect'; export { CustomFieldsAdditionalForm, - MaxChatsPerAgentContainer, + MaxChatsPerAgent, MaxChatsPerAgentDisplay, EeNumberInput, EeTextAreaInput, diff --git a/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx b/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx index ef397d8ba7c0..e23baf346f7f 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx @@ -2,6 +2,7 @@ import type { ILivechatAgent, ILivechatDepartment, ILivechatDepartmentAgents } f import { Field, FieldLabel, + FieldGroup, FieldRow, TextInput, Button, @@ -12,199 +13,229 @@ import { ContextualbarFooter, ButtonGroup, } from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useToastMessageDispatch, useRoute, useSetting, useMethod, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; -import type { FC } from 'react'; -import React, { useMemo, useRef, useState } from 'react'; +import type { SelectOption } from '@rocket.chat/fuselage'; +import { useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useToastMessageDispatch, useSetting, useMethod, useTranslation, useEndpoint, useRouter } from '@rocket.chat/ui-contexts'; +import { useQueryClient } from '@tanstack/react-query'; +import React, { useMemo } from 'react'; +import { useForm, Controller, FormProvider } from 'react-hook-form'; import { getUserEmailAddress } from '../../../../lib/getUserEmailAddress'; -import { ContextualbarScrollableContent } from '../../../components/Contextualbar'; +import { + Contextualbar, + ContextualbarTitle, + ContextualbarClose, + ContextualbarHeader, + ContextualbarScrollableContent, +} from '../../../components/Contextualbar'; import UserInfo from '../../../components/UserInfo'; -import { useForm } from '../../../hooks/useForm'; -import { MaxChatsPerAgentContainer } from '../additionalForms'; - -// TODO: TYPE: -// Department - -type dataType = { - user: Pick; -}; +import { MaxChatsPerAgent } from '../additionalForms'; type AgentEditProps = { - data: dataType; - userDepartments: { departments: Pick[] }; - availableDepartments: { departments: Pick[] }; - uid: string; - reset: () => void; + agentData: Pick; + userDepartments: Pick[]; + availableDepartments: Pick[]; }; -const AgentEdit: FC = ({ data, userDepartments, availableDepartments, uid, reset, ...props }) => { +const AgentEdit = ({ agentData, userDepartments, availableDepartments }: AgentEditProps) => { const t = useTranslation(); - const agentsRoute = useRoute('omnichannel-agents'); - const [maxChatUnsaved, setMaxChatUnsaved] = useState(); + const router = useRouter(); + const queryClient = useQueryClient(); + const voipEnabled = useSetting('VoIP_Enabled'); + const dispatchToastMessage = useToastMessageDispatch(); - const { user } = data || { user: {} }; - const { name, username, statusLivechat } = user; + const { name, username, livechat, statusLivechat } = agentData; - const email = getUserEmailAddress(user); + const email = getUserEmailAddress(agentData); - const options: [string, string][] = useMemo(() => { + const departmentsOptions: SelectOption[] = useMemo(() => { const archivedDepartment = (name: string, archived?: boolean) => (archived ? `${name} [${t('Archived')}]` : name); - return availableDepartments?.departments - ? availableDepartments.departments.map(({ _id, name, archived }) => - name ? [_id, archivedDepartment(name, archived)] : [_id, archivedDepartment(_id, archived)], - ) - : []; - }, [availableDepartments.departments, t]); - - const initialDepartmentValue = useMemo( - () => (userDepartments.departments ? userDepartments.departments.map(({ departmentId }) => departmentId) : []), - [userDepartments], + return ( + availableDepartments.map(({ _id, name, archived }) => + name ? [_id, archivedDepartment(name, archived)] : [_id, archivedDepartment(_id, archived)], + ) || [] + ); + }, [availableDepartments, t]); + + const statusOptions: SelectOption[] = useMemo( + () => [ + ['available', t('Available')], + ['not-available', t('Not_Available')], + ], + [t], ); - const saveRef = useRef({ - values: {}, - hasUnsavedChanges: false, - reset: () => undefined, - commit: () => undefined, + const initialDepartmentValue = useMemo(() => userDepartments.map(({ departmentId }) => departmentId) || [], [userDepartments]); + + const methods = useForm({ + values: { + name, + username, + email, + departments: initialDepartmentValue, + status: statusLivechat, + maxNumberSimultaneousChat: livechat?.maxNumberSimultaneousChat || 0, + voipExtension: '', + }, }); - const { reset: resetMaxChats, commit: commitMaxChats } = saveRef.current; - - const onChangeMaxChats = useMutableCallback(({ hasUnsavedChanges, ...value }) => { - saveRef.current = value; - - if (hasUnsavedChanges !== maxChatUnsaved) { - setMaxChatUnsaved(hasUnsavedChanges); - } - }); - - const { values, handlers, hasUnsavedChanges, commit } = useForm({ - departments: initialDepartmentValue, - status: statusLivechat, - maxChats: 0, - voipExtension: '', - }); - - const { handleDepartments, handleStatus, handleVoipExtension } = handlers; - const { departments, status, voipExtension } = values as { - departments: string[]; - status: ILivechatAgent['statusLivechat']; - voipExtension: string; - }; + const { + control, + handleSubmit, + reset, + formState: { isDirty }, + } = methods; const saveAgentInfo = useMethod('livechat:saveAgentInfo'); const saveAgentStatus = useEndpoint('POST', '/v1/livechat/agent.status'); - const dispatchToastMessage = useToastMessageDispatch(); - - const handleReset = useMutableCallback(() => { - reset(); - resetMaxChats(); - }); - - const handleSave = useMutableCallback(async () => { + const handleSave = useMutableCallback(async ({ status, departments, ...data }) => { try { - await saveAgentStatus({ status, agentId: uid }); - await saveAgentInfo(uid, saveRef.current.values, departments); + await saveAgentStatus({ agentId: agentData._id, status }); + await saveAgentInfo(agentData._id, data, departments); dispatchToastMessage({ type: 'success', message: t('Success') }); - agentsRoute.push({}); - reset(); + router.navigate('/omnichannel/agents'); + queryClient.invalidateQueries(['livechat-agents']); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } - commit(); - commitMaxChats(); }); + const formId = useUniqueId(); + const nameField = useUniqueId(); + const usernameField = useUniqueId(); + const emailField = useUniqueId(); + const departmentsField = useUniqueId(); + const statusField = useUniqueId(); + const voipExtensionField = useUniqueId(); + return ( - <> - - {username && ( - - - - )} - - {t('Name')} - - - - - - {t('Username')} - - } /> - - - - {t('Email')} - - } /> - - - - {t('Departments')} - - - - - - {t('Status')} - - + )} + /> + + + {MaxChatsPerAgent && } + {voipEnabled && ( + + {t('VoIP_Extension')} + + } + /> + + + )} + + + - - - - + ); }; diff --git a/apps/meteor/client/views/omnichannel/agents/AgentEditWithData.tsx b/apps/meteor/client/views/omnichannel/agents/AgentEditWithData.tsx index 543d1d7bab93..df092de6f5eb 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentEditWithData.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentEditWithData.tsx @@ -1,3 +1,4 @@ +import type { ILivechatAgent } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; @@ -7,41 +8,42 @@ import React from 'react'; import { FormSkeleton } from '../../../components/Skeleton'; import AgentEdit from './AgentEdit'; -type AgentEditWithDataProps = { - uid: string; - reload: () => void; -}; - -const AgentEditWithData = ({ uid, reload }: AgentEditWithDataProps): ReactElement => { +const AgentEditWithData = ({ uid }: { uid: ILivechatAgent['_id'] }): ReactElement => { const t = useTranslation(); - const getDepartments = useEndpoint('GET', '/v1/livechat/department'); - - const getAgent = useEndpoint('GET', '/v1/livechat/users/agent/:_id', { _id: uid }); + const getAvailableDepartments = useEndpoint('GET', '/v1/livechat/department'); + const getAgentById = useEndpoint('GET', '/v1/livechat/users/agent/:_id', { _id: uid }); const getAgentDepartments = useEndpoint('GET', '/v1/livechat/agents/:agentId/departments', { agentId: uid }); - const { data, isInitialLoading: isLoading, error } = useQuery(['getAgent'], async () => getAgent()); + const { data, isLoading, error } = useQuery(['livechat-getAgentById', uid], async () => getAgentById(), { refetchOnWindowFocus: false }); + const { - data: userDepartments, - isLoading: isUserDepartmentsLoading, - error: userDepartmentsError, - } = useQuery({ queryKey: ['getAgentDepartments'], queryFn: async () => getAgentDepartments(), cacheTime: 0 }); + data: agentDepartments, + isLoading: agentDepartmentsLoading, + error: agentsDepartmentsError, + } = useQuery(['livechat-getAgentDepartments', uid], async () => getAgentDepartments(), { refetchOnWindowFocus: false }); const { data: availableDepartments, - isLoading: isAvailableDepartmentsLoading, + isLoading: availableDepartmentsLoading, error: availableDepartmentsError, - } = useQuery(['getDepartments'], async () => getDepartments({ showArchived: 'true' })); + } = useQuery(['livechat-getAvailableDepartments'], async () => getAvailableDepartments({ showArchived: 'true' })); - if (isLoading || isAvailableDepartmentsLoading || isUserDepartmentsLoading || !userDepartments || !availableDepartments) { + if (isLoading || availableDepartmentsLoading || agentDepartmentsLoading || !agentDepartments || !availableDepartments) { return ; } - if (error || userDepartmentsError || availableDepartmentsError || !data || !data.user) { + if (error || agentsDepartmentsError || availableDepartmentsError || !data?.user) { return {t('User_not_found')}; } - return ; + return ( + + ); }; export default AgentEditWithData; diff --git a/apps/meteor/client/views/omnichannel/agents/AgentInfo.tsx b/apps/meteor/client/views/omnichannel/agents/AgentInfo.tsx index 5c8765a1b9f1..09723ee31602 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentInfo.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentInfo.tsx @@ -1,63 +1,83 @@ -import { Box, Margins, ButtonGroup } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; +import { Box, Margins, ButtonGroup, ContextualbarSkeleton } from '@rocket.chat/fuselage'; +import { useEndpoint, useRouter, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; import type { HTMLAttributes } from 'react'; -import React, { memo } from 'react'; +import React from 'react'; -import { ContextualbarScrollableContent } from '../../../components/Contextualbar'; -import { FormSkeleton } from '../../../components/Skeleton'; +import { + Contextualbar, + ContextualbarTitle, + ContextualbarClose, + ContextualbarHeader, + ContextualbarScrollableContent, +} from '../../../components/Contextualbar'; import UserInfo from '../../../components/UserInfo'; import { UserStatus } from '../../../components/UserStatus'; -import { AsyncStatePhase } from '../../../hooks/useAsyncState'; -import { useEndpointData } from '../../../hooks/useEndpointData'; import { MaxChatsPerAgentDisplay } from '../additionalForms'; +import AgentInfoAction from './AgentInfoAction'; +import { useRemoveAgent } from './hooks/useRemoveAgent'; type AgentInfoProps = { uid: string; } & Omit, 'is'>; -const AgentInfo = memo(function AgentInfo({ uid, children, ...props }) { +const AgentInfo = ({ uid }: AgentInfoProps) => { const t = useTranslation(); - const result = useEndpointData('/v1/livechat/users/agent/:_id', { keys: { _id: uid } }); + const router = useRouter(); + const getAgentById = useEndpoint('GET', '/v1/livechat/users/agent/:_id', { _id: uid }); + const { data, isLoading, isError } = useQuery(['livechat-getAgentInfoById', uid], async () => getAgentById(), { + refetchOnWindowFocus: false, + }); - if (result.phase === AsyncStatePhase.LOADING) { - return ; + const handleDelete = useRemoveAgent(uid); + + if (isLoading) { + return ; } - if (result.phase === AsyncStatePhase.REJECTED) { + if (isError) { return {t('User_not_found')}; } - const { user } = result.value; - const { username, statusLivechat, status: userStatus } = user; + const { username, statusLivechat, status: userStatus } = data?.user; return ( - - {username && ( - - - - )} - - - {children} - - - - - } /> - - - {statusLivechat && ( - <> - {t('Livechat_status')} - {t(statusLivechat === 'available' ? 'Available' : 'Not_Available')} - + + + {t('User_Info')} + router.navigate('/omnichannel/agents')} /> + + + {username && ( + + + )} - - - - + + router.navigate(`/omnichannel/agents/edit/${uid}`)} + icon='edit' + /> + + + + + } /> + + {statusLivechat && ( + <> + {t('Livechat_status')} + {t(statusLivechat === 'available' ? 'Available' : 'Not_Available')} + + )} + {MaxChatsPerAgentDisplay && } + + + ); -}); +}; export default AgentInfo; diff --git a/apps/meteor/client/views/omnichannel/agents/AgentInfoActions.tsx b/apps/meteor/client/views/omnichannel/agents/AgentInfoActions.tsx deleted file mode 100644 index 3a8422be381d..000000000000 --- a/apps/meteor/client/views/omnichannel/agents/AgentInfoActions.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, useRouteParameter, useRoute, useTranslation } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; -import React from 'react'; - -import GenericModal from '../../../components/GenericModal'; -import { useEndpointAction } from '../../../hooks/useEndpointAction'; -import AgentInfoAction from './AgentInfoAction'; - -const AgentInfoActions = ({ reload }: { reload: () => void }): ReactElement => { - const t = useTranslation(); - const _id = useRouteParameter('id') ?? ''; - const agentsRoute = useRoute('omnichannel-agents'); - const deleteAction = useEndpointAction('DELETE', '/v1/livechat/users/agent/:_id', { keys: { _id } }); - const setModal = useSetModal(); - const dispatchToastMessage = useToastMessageDispatch(); - - const handleRemoveClick = useMutableCallback(async () => { - const result = await deleteAction(); - if (result.success === true) { - agentsRoute.push({}); - reload(); - } - }); - - const handleDelete = useMutableCallback((e) => { - e.stopPropagation(); - const onDeleteAgent = async (): Promise => { - try { - await handleRemoveClick(); - dispatchToastMessage({ type: 'success', message: t('Agent_removed') }); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - setModal(); - }; - - setModal( setModal()} confirmText={t('Delete')} />); - }); - - const handleEditClick = useMutableCallback(() => - agentsRoute.push({ - context: 'edit', - id: _id, - }), - ); - - return ( - <> - - - - ); -}; - -export default AgentInfoActions; diff --git a/apps/meteor/client/views/omnichannel/agents/AgentsPage.tsx b/apps/meteor/client/views/omnichannel/agents/AgentsPage.tsx index 7640077baaba..d276413b9861 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentsPage.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentsPage.tsx @@ -1,24 +1,20 @@ import { usePermission, useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import React, { useRef, useCallback } from 'react'; +import React from 'react'; import Page from '../../../components/Page'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; -import AgentsTab from './AgentsTab'; +import AgentEditWithData from './AgentEditWithData'; +import AgentInfo from './AgentInfo'; import AgentsTable from './AgentsTable/AgentsTable'; const AgentsPage = (): ReactElement => { const t = useTranslation(); - const reload = useRef(() => null); const canViewAgents = usePermission('manage-livechat-agents'); const context = useRouteParameter('context'); const id = useRouteParameter('id'); - const handleReload = useCallback(() => { - reload.current(); - }, [reload]); - if (!canViewAgents) { return ; } @@ -28,10 +24,11 @@ const AgentsPage = (): ReactElement => { - + - {context && id && } + {id && context === 'edit' && } + {id && context === 'info' && } ); }; diff --git a/apps/meteor/client/views/omnichannel/agents/AgentsTab.tsx b/apps/meteor/client/views/omnichannel/agents/AgentsTab.tsx deleted file mode 100644 index 1ba6d7537e9a..000000000000 --- a/apps/meteor/client/views/omnichannel/agents/AgentsTab.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; -import React, { useCallback } from 'react'; - -import { Contextualbar, ContextualbarHeader, ContextualbarClose, ContextualbarTitle } from '../../../components/Contextualbar'; -import AgentEditWithData from './AgentEditWithData'; -import AgentInfo from './AgentInfo'; -import AgentInfoActions from './AgentInfoActions'; - -type AgentsTabProps = { - reload: () => void; - context: string; - id: string; -}; - -const AgentsTab = ({ reload, context, id }: AgentsTabProps): ReactElement => { - const t = useTranslation(); - const agentsRoute = useRoute('omnichannel-agents'); - - const handleClose = useCallback((): void => { - agentsRoute.push({}); - }, [agentsRoute]); - - return ( - - - - {context === 'edit' && t('Edit_User')} - {context === 'info' && t('User_Info')} - - - - - {context === 'edit' && } - {context === 'info' && ( - - - - )} - - ); -}; - -export default AgentsTab; diff --git a/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTable.tsx b/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTable.tsx index 215155e97cca..cfabfb91f787 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTable.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTable.tsx @@ -2,8 +2,7 @@ import { Pagination } from '@rocket.chat/fuselage'; import { useDebouncedValue, useMediaQuery, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; import { hashQueryKey } from '@tanstack/react-query'; -import type { MutableRefObject } from 'react'; -import React, { useMemo, useState, useEffect } from 'react'; +import React, { useMemo, useState } from 'react'; import FilterByText from '../../../../components/FilterByText'; import GenericNoResults from '../../../../components/GenericNoResults/GenericNoResults'; @@ -22,7 +21,7 @@ import AddAgent from './AddAgent'; import AgentsTableRow from './AgentsTableRow'; // TODO: missing error state -const AgentsTable = ({ reload }: { reload: MutableRefObject<() => void> }) => { +const AgentsTable = () => { const t = useTranslation(); const [filter, setFilter] = useState(''); @@ -41,11 +40,6 @@ const AgentsTable = ({ reload }: { reload: MutableRefObject<() => void> }) => { const [defaultQuery] = useState(hashQueryKey([query])); const queryHasChanged = defaultQuery !== hashQueryKey([query]); - useEffect(() => { - reload.current = refetch; - }, [reload, refetch]); - reload.current = refetch; - const onHeaderClick = useMutableCallback((id) => { if (sortBy === id) { setSort(id, sortDirection === 'asc' ? 'desc' : 'asc'); @@ -100,11 +94,11 @@ const AgentsTable = ({ reload }: { reload: MutableRefObject<() => void> }) => { )} {isSuccess && data?.users.length > 0 && ( <> - + {headers} {data?.users.map((user) => ( - + ))} diff --git a/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTableRow.tsx b/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTableRow.tsx index d65efacbb1c8..1bad8cbcc62e 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTableRow.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTableRow.tsx @@ -1,17 +1,15 @@ -import { Box } from '@rocket.chat/fuselage'; -import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; -import { useQueryClient } from '@tanstack/react-query'; +import { Box, IconButton } from '@rocket.chat/fuselage'; +import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import React, { useCallback } from 'react'; +import React from 'react'; import { GenericTableRow, GenericTableCell } from '../../../../components/GenericTable'; import UserAvatar from '../../../../components/avatar/UserAvatar'; -import RemoveAgentButton from './RemoveAgentButton'; +import { useRemoveAgent } from '../hooks/useRemoveAgent'; const AgentsTableRow = ({ - user: { _id, name, username, avatarETag, emails, statusLivechat, departments }, + user: { _id, name, username, avatarETag, emails, statusLivechat }, mediaQuery, - reload, }: { user: { _id: string; @@ -20,30 +18,16 @@ const AgentsTableRow = ({ avatarETag?: string; emails?: { address: string }[]; statusLivechat: string; - departments: string[]; }; mediaQuery: boolean; - reload: () => void; }): ReactElement => { const t = useTranslation(); - const agentsRoute = useRoute('omnichannel-agents'); - const queryClient = useQueryClient(); + const router = useRouter(); - const onRowClick = useCallback(() => { - agentsRoute.push({ - context: 'info', - id: _id, - }); - }, [_id, agentsRoute]); - - const onAgentRemoved = useCallback(() => { - departments.forEach((departmentId) => { - queryClient.removeQueries(['/v1/livechat/department/:_id', departmentId]); - }); - }, [queryClient, departments]); + const handleDelete = useRemoveAgent(_id); return ( - + router.navigate(`/omnichannel/agents/info/${_id}`)}> {username && } @@ -71,7 +55,17 @@ const AgentsTableRow = ({ )} {emails?.length && emails[0].address} {statusLivechat === 'available' ? t('Available') : t('Not_Available')} - + + { + e.stopPropagation(); + handleDelete(); + }} + /> + ); }; diff --git a/apps/meteor/client/views/omnichannel/agents/AgentsTable/RemoveAgentButton.tsx b/apps/meteor/client/views/omnichannel/agents/AgentsTable/RemoveAgentButton.tsx deleted file mode 100644 index 81956a058f82..000000000000 --- a/apps/meteor/client/views/omnichannel/agents/AgentsTable/RemoveAgentButton.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { IconButton } from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; -import React from 'react'; - -import GenericModal from '../../../../components/GenericModal'; -import { GenericTableCell } from '../../../../components/GenericTable'; -import { useEndpointAction } from '../../../../hooks/useEndpointAction'; - -type RemoveAgentButtonProps = { - _id: string; - reload: () => void; - onAgentRemoved?: () => void; -}; - -const RemoveAgentButton = ({ _id, reload, onAgentRemoved }: RemoveAgentButtonProps): ReactElement => { - const deleteAction = useEndpointAction('DELETE', '/v1/livechat/users/agent/:_id', { keys: { _id } }); - const setModal = useSetModal(); - const dispatchToastMessage = useToastMessageDispatch(); - const t = useTranslation(); - - const handleRemoveClick = useMutableCallback(async () => { - const result = await deleteAction(); - if (result.success === true) { - reload(); - onAgentRemoved?.(); - } - }); - - const handleDelete = useMutableCallback((e) => { - e.stopPropagation(); - const onDeleteAgent = async (): Promise => { - try { - await handleRemoveClick(); - dispatchToastMessage({ type: 'success', message: t('Agent_removed') }); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - setModal(); - }; - - setModal( setModal()} confirmText={t('Delete')} />); - }); - - return ( - - - - ); -}; - -export default RemoveAgentButton; diff --git a/apps/meteor/client/views/omnichannel/agents/hooks/useRemoveAgent.tsx b/apps/meteor/client/views/omnichannel/agents/hooks/useRemoveAgent.tsx new file mode 100644 index 000000000000..206d7aab2668 --- /dev/null +++ b/apps/meteor/client/views/omnichannel/agents/hooks/useRemoveAgent.tsx @@ -0,0 +1,36 @@ +import type { ILivechatAgent } from '@rocket.chat/core-typings'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useSetModal, useToastMessageDispatch, useTranslation, useRouter, useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQueryClient } from '@tanstack/react-query'; +import React from 'react'; + +import GenericModal from '../../../../components/GenericModal'; + +export const useRemoveAgent = (uid: ILivechatAgent['_id']) => { + const t = useTranslation(); + const router = useRouter(); + const setModal = useSetModal(); + const queryClient = useQueryClient(); + const dispatchToastMessage = useToastMessageDispatch(); + + const deleteAction = useEndpoint('DELETE', '/v1/livechat/users/agent/:_id', { _id: uid }); + + const handleDelete = useMutableCallback(() => { + const onDeleteAgent = async () => { + try { + await deleteAction(); + dispatchToastMessage({ type: 'success', message: t('Agent_removed') }); + router.navigate('/omnichannel/agents'); + queryClient.invalidateQueries(['livechat-agents']); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } finally { + setModal(); + } + }; + + setModal( setModal()} confirmText={t('Delete')} />); + }); + + return handleDelete; +}; diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgent.tsx b/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgent.tsx index 1894b1367c15..75a2656dc4ea 100644 --- a/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgent.tsx +++ b/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgent.tsx @@ -1,21 +1,32 @@ import { NumberInput, Field, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import type { FC } from 'react'; +import type { ComponentProps } from 'react'; import React from 'react'; +import { useFormContext, Controller } from 'react-hook-form'; -const MaxChatsPerAgent: FC<{ - values: { maxNumberSimultaneousChat: number }; - handlers: { handleMaxNumberSimultaneousChat: () => void }; -}> = ({ values, handlers }) => { +import { useHasLicenseModule } from '../../hooks/useHasLicenseModule'; + +const MaxChatsPerAgent = ({ className }: { className?: ComponentProps['className'] }) => { const t = useTranslation(); - const { maxNumberSimultaneousChat } = values; - const { handleMaxNumberSimultaneousChat } = handlers; + const { control } = useFormContext(); + const hasLicense = useHasLicenseModule('livechat-enterprise'); + + const maxChatsField = useUniqueId(); + + if (!hasLicense) { + return null; + } return ( - - {t('Max_number_of_chats_per_agent')} + + {t('Max_number_of_chats_per_agent')} - + } + /> ); diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentContainer.js b/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentContainer.js deleted file mode 100644 index f8e70e020c92..000000000000 --- a/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentContainer.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; - -import { useForm } from '../../../../client/hooks/useForm'; -import { useHasLicenseModule } from '../../hooks/useHasLicenseModule'; -import MaxChatsPerAgent from './MaxChatsPerAgent'; - -const MaxChatsPerAgentContainer = ({ data: { livechat: { maxNumberSimultaneousChat = '' } = {} } = {}, onChange }) => { - const hasLicense = useHasLicenseModule('livechat-enterprise'); - - const { values, handlers, hasUnsavedChanges, commit, reset } = useForm({ - maxNumberSimultaneousChat, - }); - - onChange({ values, hasUnsavedChanges, commit, reset }); - - if (!hasLicense) { - return null; - } - - return ; -}; - -export default MaxChatsPerAgentContainer; diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentDisplay.js b/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentDisplay.tsx similarity index 77% rename from apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentDisplay.js rename to apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentDisplay.tsx index f166678607aa..286b1a344d3b 100644 --- a/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentDisplay.js +++ b/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentDisplay.tsx @@ -4,7 +4,7 @@ import React from 'react'; import UserInfo from '../../../../client/components/UserInfo'; import { useHasLicenseModule } from '../../hooks/useHasLicenseModule'; -const MaxChatsPerAgentDisplay = ({ data: { livechat: { maxNumberSimultaneousChat = 0 } = {} } = {} }) => { +const MaxChatsPerAgentDisplay = ({ maxNumberSimultaneousChat = 0 }) => { const t = useTranslation(); const hasLicense = useHasLicenseModule('livechat-enterprise'); @@ -12,12 +12,12 @@ const MaxChatsPerAgentDisplay = ({ data: { livechat: { maxNumberSimultaneousChat return null; } - return maxNumberSimultaneousChat ? ( + return ( <> {t('Max_number_of_chats_per_agent')} {maxNumberSimultaneousChat} - ) : null; + ); }; export default MaxChatsPerAgentDisplay; diff --git a/packages/core-typings/src/ILivechatAgent.ts b/packages/core-typings/src/ILivechatAgent.ts index 68a99c2723d9..f106239400b1 100644 --- a/packages/core-typings/src/ILivechatAgent.ts +++ b/packages/core-typings/src/ILivechatAgent.ts @@ -7,12 +7,11 @@ export enum ILivechatAgentStatus { export interface ILivechatAgent extends IUser { statusLivechat: ILivechatAgentStatus; - livechat: { + livechat?: { maxNumberSimultaneousChat: number; }; livechatCount: number; lastRoutingTime: Date; livechatStatusSystemModified?: boolean; - openBusinessHours?: string[]; } From dd5fd6d2c8e9b9a3746dd81580828c6dd64e4f93 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Tue, 5 Dec 2023 16:40:00 -0300 Subject: [PATCH 8/9] feat: Skip to main content shortcut and `useDocumentTitle` (#30680) Co-authored-by: Guilherme Gazzo --- .changeset/kind-beers-share.md | 7 +++ .../client/components/Page/PageHeader.tsx | 8 +-- apps/meteor/client/sidebar/Item/Condensed.tsx | 2 +- apps/meteor/client/sidebar/Item/Extended.tsx | 4 +- apps/meteor/client/sidebar/Item/Medium.tsx | 2 +- apps/meteor/client/startup/unread.ts | 5 -- .../client/views/directory/DirectoryPage.tsx | 2 +- .../channels/ChannelsTable/ChannelsTable.tsx | 2 +- .../tabs/teams/TeamsTable/TeamsTable.tsx | 2 +- .../tabs/users/UsersTable/UsersTable.tsx | 2 +- .../views/home/cards/CustomContentCard.tsx | 12 ++-- .../client/views/room/Header/RoomTitle.tsx | 18 +++--- apps/meteor/client/views/root/AppLayout.tsx | 7 ++- .../views/root/DocumentTitleWrapper.tsx | 55 +++++++++++++++++++ .../root/MainLayout/AccessibilityShortcut.tsx | 33 +++++++++++ .../root/MainLayout/LayoutWithSidebar.tsx | 7 ++- .../views/root/hooks/useUnreadMessages.ts | 14 +++++ .../rocketchat-i18n/i18n/en.i18n.json | 1 + apps/meteor/tests/e2e/homepage.spec.ts | 12 ++-- .../src/hooks/useDocumentTitle.spec.ts | 26 +++++++++ .../ui-client/src/hooks/useDocumentTitle.ts | 54 ++++++++++++++++++ packages/ui-client/src/index.ts | 1 + .../web-ui-registration/src/GuestForm.tsx | 2 + .../web-ui-registration/src/LoginForm.tsx | 3 + .../src/RegisterSecretPageRouter.tsx | 5 ++ .../src/ResetPasswordForm.tsx | 3 + 26 files changed, 249 insertions(+), 40 deletions(-) create mode 100644 .changeset/kind-beers-share.md create mode 100644 apps/meteor/client/views/root/DocumentTitleWrapper.tsx create mode 100644 apps/meteor/client/views/root/MainLayout/AccessibilityShortcut.tsx create mode 100644 apps/meteor/client/views/root/hooks/useUnreadMessages.ts create mode 100644 packages/ui-client/src/hooks/useDocumentTitle.spec.ts create mode 100644 packages/ui-client/src/hooks/useDocumentTitle.ts diff --git a/.changeset/kind-beers-share.md b/.changeset/kind-beers-share.md new file mode 100644 index 000000000000..b7871e568d10 --- /dev/null +++ b/.changeset/kind-beers-share.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/ui-client": minor +"@rocket.chat/web-ui-registration": minor +--- + +feat: Skip to main content shortcut and useDocumentTitle diff --git a/apps/meteor/client/components/Page/PageHeader.tsx b/apps/meteor/client/components/Page/PageHeader.tsx index 6725c2da6f5c..25d20381e52e 100644 --- a/apps/meteor/client/components/Page/PageHeader.tsx +++ b/apps/meteor/client/components/Page/PageHeader.tsx @@ -1,6 +1,5 @@ import { Box, IconButton } from '@rocket.chat/fuselage'; -import { useAutoFocus } from '@rocket.chat/fuselage-hooks'; -import { HeaderToolbox } from '@rocket.chat/ui-client'; +import { HeaderToolbox, useDocumentTitle } from '@rocket.chat/ui-client'; import { useLayout, useTranslation } from '@rocket.chat/ui-contexts'; import type { FC, ComponentProps, ReactNode } from 'react'; import React, { useContext } from 'react'; @@ -18,12 +17,11 @@ const PageHeader: FC = ({ children = undefined, title, onClickB const t = useTranslation(); const [border] = useContext(PageContext); const { isMobile } = useLayout(); - const headerAutoFocus = useAutoFocus(); + + useDocumentTitle(typeof title === 'string' ? title : undefined); return ( = ({ icon, title = '', avatar, actions, href {badges && {badges}} {menu && ( - {menuVisibility ? menu() : } + {menuVisibility ? menu() : } )} diff --git a/apps/meteor/client/sidebar/Item/Extended.tsx b/apps/meteor/client/sidebar/Item/Extended.tsx index c997a48b5b4a..73493a4aee8f 100644 --- a/apps/meteor/client/sidebar/Item/Extended.tsx +++ b/apps/meteor/client/sidebar/Item/Extended.tsx @@ -54,7 +54,7 @@ const Extended: VFC = ({ }; return ( - + {avatar && {avatar}} @@ -72,7 +72,7 @@ const Extended: VFC = ({ {badges} {menu && ( - {menuVisibility ? menu() : } + {menuVisibility ? menu() : } )} diff --git a/apps/meteor/client/sidebar/Item/Medium.tsx b/apps/meteor/client/sidebar/Item/Medium.tsx index 2c97b890988f..6feed3071ffc 100644 --- a/apps/meteor/client/sidebar/Item/Medium.tsx +++ b/apps/meteor/client/sidebar/Item/Medium.tsx @@ -42,7 +42,7 @@ const Medium: VFC = ({ icon, title = '', avatar, actions, href, bad {badges && {badges}} {menu && ( - {menuVisibility ? menu() : } + {menuVisibility ? menu() : } )} diff --git a/apps/meteor/client/startup/unread.ts b/apps/meteor/client/startup/unread.ts index 6c076e4ba107..d9c2a35efab5 100644 --- a/apps/meteor/client/startup/unread.ts +++ b/apps/meteor/client/startup/unread.ts @@ -5,7 +5,6 @@ import { Session } from 'meteor/session'; import { Tracker } from 'meteor/tracker'; import { ChatSubscription, ChatRoom } from '../../app/models/client'; -import { settings } from '../../app/settings/client'; import { getUserPreference } from '../../app/utils/client'; import { fireGlobalEvent } from '../lib/utils/fireGlobalEvent'; @@ -78,13 +77,9 @@ Meteor.startup(() => { const updateFavicon = manageFavicon(); Tracker.autorun(() => { - const siteName = settings.get('Site_Name') ?? ''; - const unread = Session.get('unread'); fireGlobalEvent('unread-changed', unread); updateFavicon(unread); - - document.title = unread === '' ? siteName : `(${unread}) ${siteName}`; }); }); diff --git a/apps/meteor/client/views/directory/DirectoryPage.tsx b/apps/meteor/client/views/directory/DirectoryPage.tsx index 2bcd32a2a5e8..d260971dde2c 100644 --- a/apps/meteor/client/views/directory/DirectoryPage.tsx +++ b/apps/meteor/client/views/directory/DirectoryPage.tsx @@ -56,8 +56,8 @@ const DirectoryPage = (): ReactElement => { )} - {tab === 'users' && } {tab === 'channels' && } + {tab === 'users' && } {tab === 'teams' && } {federationEnabled && tab === 'external' && } diff --git a/apps/meteor/client/views/directory/tabs/channels/ChannelsTable/ChannelsTable.tsx b/apps/meteor/client/views/directory/tabs/channels/ChannelsTable/ChannelsTable.tsx index 0d2b9f8db289..8ec510eed76d 100644 --- a/apps/meteor/client/views/directory/tabs/channels/ChannelsTable/ChannelsTable.tsx +++ b/apps/meteor/client/views/directory/tabs/channels/ChannelsTable/ChannelsTable.tsx @@ -96,7 +96,7 @@ const ChannelsTable = () => { return ( <> - setText(text)} /> + setText(text)} /> {isLoading && ( {headers} diff --git a/apps/meteor/client/views/directory/tabs/teams/TeamsTable/TeamsTable.tsx b/apps/meteor/client/views/directory/tabs/teams/TeamsTable/TeamsTable.tsx index 160ce8e8cc59..f33b359e4b07 100644 --- a/apps/meteor/client/views/directory/tabs/teams/TeamsTable/TeamsTable.tsx +++ b/apps/meteor/client/views/directory/tabs/teams/TeamsTable/TeamsTable.tsx @@ -73,7 +73,7 @@ const TeamsTable = () => { return ( <> - setText(text)} /> + setText(text)} /> {isLoading && ( {headers} diff --git a/apps/meteor/client/views/directory/tabs/users/UsersTable/UsersTable.tsx b/apps/meteor/client/views/directory/tabs/users/UsersTable/UsersTable.tsx index c67d408aa2a3..5b69a59909ca 100644 --- a/apps/meteor/client/views/directory/tabs/users/UsersTable/UsersTable.tsx +++ b/apps/meteor/client/views/directory/tabs/users/UsersTable/UsersTable.tsx @@ -95,7 +95,7 @@ const UsersTable = ({ workspace = 'local' }): ReactElement => { return ( <> - setText(text)} /> + setText(text)} /> {isLoading && ( {headers} diff --git a/apps/meteor/client/views/home/cards/CustomContentCard.tsx b/apps/meteor/client/views/home/cards/CustomContentCard.tsx index 44f8c6262406..7683f1e1c300 100644 --- a/apps/meteor/client/views/home/cards/CustomContentCard.tsx +++ b/apps/meteor/client/views/home/cards/CustomContentCard.tsx @@ -13,10 +13,10 @@ const CustomContentCard = (): ReactElement | null => { const { data } = useIsEnterprise(); const isAdmin = useRole('admin'); - const customContentBody = String(useSetting('Layout_Home_Body')); + const customContentBody = useSetting('Layout_Home_Body'); const isCustomContentBodyEmpty = customContentBody === ''; - const isCustomContentVisible = Boolean(useSetting('Layout_Home_Custom_Block_Visible')); - const isCustomContentOnly = Boolean(useSetting('Layout_Custom_Body_Only')); + const isCustomContentVisible = useSetting('Layout_Home_Custom_Block_Visible'); + const isCustomContentOnly = useSetting('Layout_Custom_Body_Only'); const settingsRoute = useRoute('admin-settings'); @@ -55,14 +55,12 @@ const CustomContentCard = (): ReactElement | null => { return ( - + {willNotShowCustomContent ? t('Not_Visible_To_Workspace') : t('Visible_To_Workspace')} - - {isCustomContentBodyEmpty ? t('Homepage_Custom_Content_Default_Message') : } - + {isCustomContentBodyEmpty ? t('Homepage_Custom_Content_Default_Message') : } + ); +}; + +export default AccessibilityShortcut; diff --git a/apps/meteor/client/views/root/MainLayout/LayoutWithSidebar.tsx b/apps/meteor/client/views/root/MainLayout/LayoutWithSidebar.tsx index 5a5ea0998c67..1edd6966d065 100644 --- a/apps/meteor/client/views/root/MainLayout/LayoutWithSidebar.tsx +++ b/apps/meteor/client/views/root/MainLayout/LayoutWithSidebar.tsx @@ -6,6 +6,7 @@ import type { ReactElement, ReactNode } from 'react'; import React, { useEffect, useRef } from 'react'; import Sidebar from '../../../sidebar'; +import AccessibilityShortcut from './AccessibilityShortcut'; const LayoutWithSidebar = ({ children }: { children: ReactNode }): ReactElement => { const { isEmbedded: embeddedLayout } = useLayout(); @@ -46,10 +47,14 @@ const LayoutWithSidebar = ({ children }: { children: ReactNode }): ReactElement className={[embeddedLayout ? 'embedded-view' : undefined, 'menu-nav'].filter(Boolean).join(' ')} aria-hidden={Boolean(modal)} > + {!removeSidenav && } -
+
{children}
diff --git a/apps/meteor/client/views/root/hooks/useUnreadMessages.ts b/apps/meteor/client/views/root/hooks/useUnreadMessages.ts new file mode 100644 index 000000000000..0f2ee34b8c4c --- /dev/null +++ b/apps/meteor/client/views/root/hooks/useUnreadMessages.ts @@ -0,0 +1,14 @@ +import { useSession, useTranslation } from '@rocket.chat/ui-contexts'; + +export const useUnreadMessages = (): string | undefined => { + const t = useTranslation(); + const unreadMessages = useSession('unread'); + + return (() => { + if (unreadMessages === '') { + return undefined; + } + + return t('unread_messages_counter', { count: unreadMessages }); + })(); +}; diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 6898eb26e4a1..33df4302103b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -4775,6 +4775,7 @@ "Size": "Size", "Skin_tone": "Skin tone", "Skip": "Skip", + "Skip_to_main_content": "Skip to main content", "SLA_Policy": "SLA Policy", "SLA_Policies": "SLA Policies", "SLA_removed": "SLA removed", diff --git a/apps/meteor/tests/e2e/homepage.spec.ts b/apps/meteor/tests/e2e/homepage.spec.ts index 4f8f9d09a2f3..89aa905744df 100644 --- a/apps/meteor/tests/e2e/homepage.spec.ts +++ b/apps/meteor/tests/e2e/homepage.spec.ts @@ -50,7 +50,7 @@ test.describe.serial('homepage', () => { test('visibility and button functionality in custom body with empty custom content', async () => { await test.step('expect default value in custom body', async () => { await expect( - adminPage.locator('role=status[name="Admins may insert content html to be rendered in this white space."]'), + adminPage.locator('div >> text="Admins may insert content html to be rendered in this white space."'), ).toBeVisible(); }); @@ -60,7 +60,7 @@ test.describe.serial('homepage', () => { }); await test.step('expect visibility tag to show "not visible"', async () => { - await expect(adminPage.locator('role=status[name="Not visible to workspace"]')).toBeVisible(); + await expect(adminPage.locator('span >> text="Not visible to workspace"')).toBeVisible(); }); }); }); @@ -72,7 +72,7 @@ test.describe.serial('homepage', () => { test('visibility and button functionality in custom body with custom content', async () => { await test.step('expect custom body to be visible', async () => { - await expect(adminPage.locator('role=status[name="Hello admin"]')).toBeVisible(); + await expect(adminPage.locator('div >> text="Hello admin"')).toBeVisible(); }); await test.step('expect correct state for card buttons', async () => { @@ -101,7 +101,7 @@ test.describe.serial('homepage', () => { }); await test.step('expect visibility tag to show "visible to workspace"', async () => { - await expect(adminPage.locator('role=status[name="Visible to workspace"]')).toBeVisible(); + await expect(adminPage.locator('span >> text="Visible to workspace"')).toBeVisible(); }); }); }); @@ -188,7 +188,7 @@ test.describe.serial('homepage', () => { }); test('expect custom body to be visible', async () => { - await expect(regularUserPage.locator('role=status[name="Hello"]')).toBeVisible(); + await expect(regularUserPage.locator('div >> text="Hello"')).toBeVisible(); }); test.describe('enterprise edition', () => { @@ -208,7 +208,7 @@ test.describe.serial('homepage', () => { }); await test.step('expect custom body to be visible', async () => { - await expect(regularUserPage.locator('role=status[name="Hello"]')).toBeVisible(); + await expect(regularUserPage.locator('div >> text="Hello"')).toBeVisible(); }); }); }); diff --git a/packages/ui-client/src/hooks/useDocumentTitle.spec.ts b/packages/ui-client/src/hooks/useDocumentTitle.spec.ts new file mode 100644 index 000000000000..e5df07fb354c --- /dev/null +++ b/packages/ui-client/src/hooks/useDocumentTitle.spec.ts @@ -0,0 +1,26 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { useDocumentTitle } from './useDocumentTitle'; + +const DEFAULT_TITLE = 'Default Title'; +const EXAMPLE_TITLE = 'Example Title'; + +it('should return the default title', () => { + const { result } = renderHook(() => useDocumentTitle(DEFAULT_TITLE)); + + expect(result.current.title).toBe(DEFAULT_TITLE); +}); + +it('should return the default title and empty key value if refocus param is false', () => { + const { result } = renderHook(() => useDocumentTitle(DEFAULT_TITLE, false)); + + expect(result.current.title).toBe(DEFAULT_TITLE); + expect(result.current.key).toBe(''); +}); + +it('should return the default title and the example title concatenated', () => { + renderHook(() => useDocumentTitle(DEFAULT_TITLE)); + const { result } = renderHook(() => useDocumentTitle(EXAMPLE_TITLE)); + + expect(result.current.title).toBe(`${EXAMPLE_TITLE} - ${DEFAULT_TITLE}`); +}); diff --git a/packages/ui-client/src/hooks/useDocumentTitle.ts b/packages/ui-client/src/hooks/useDocumentTitle.ts new file mode 100644 index 000000000000..5c5aca8b2296 --- /dev/null +++ b/packages/ui-client/src/hooks/useDocumentTitle.ts @@ -0,0 +1,54 @@ +import { Emitter } from '@rocket.chat/emitter'; +import { useCallback, useEffect } from 'react'; +import { useSyncExternalStore } from 'use-sync-external-store/shim'; + +const ee = new Emitter<{ + change: void; +}>(); + +const titles = new Set<{ + title?: string; + refocus?: boolean; +}>(); + +const useReactiveDocumentTitle = (): string => + useSyncExternalStore( + useCallback((callback) => ee.on('change', callback), []), + (): string => + Array.from(titles) + .reverse() + .map(({ title }) => title) + .join(' - '), + ); + +const useReactiveDocumentTitleKey = (): string => + useSyncExternalStore( + useCallback((callback) => ee.on('change', callback), []), + (): string => + Array.from(titles) + .filter(({ refocus }) => refocus) + .map(({ title }) => title) + .join(' - '), + ); + +export const useDocumentTitle = (documentTitle?: string, refocus = true) => { + useEffect(() => { + const titleObj = { + title: documentTitle, + refocus, + }; + + if (titleObj.title) { + titles.add(titleObj); + } + + ee.emit('change'); + + return () => { + titles.delete(titleObj); + ee.emit('change'); + }; + }, [documentTitle, refocus]); + + return { title: useReactiveDocumentTitle(), key: useReactiveDocumentTitleKey() }; +}; diff --git a/packages/ui-client/src/index.ts b/packages/ui-client/src/index.ts index 0e4454d38dbf..c04e795f6ab5 100644 --- a/packages/ui-client/src/index.ts +++ b/packages/ui-client/src/index.ts @@ -1,3 +1,4 @@ export * from './components'; export * from './hooks/useFeaturePreview'; export * from './hooks/useFeaturePreviewList'; +export * from './hooks/useDocumentTitle'; diff --git a/packages/web-ui-registration/src/GuestForm.tsx b/packages/web-ui-registration/src/GuestForm.tsx index 59df56837d86..d6b5fe15f135 100644 --- a/packages/web-ui-registration/src/GuestForm.tsx +++ b/packages/web-ui-registration/src/GuestForm.tsx @@ -1,11 +1,13 @@ import { Button, ButtonGroup } from '@rocket.chat/fuselage'; import { Form } from '@rocket.chat/layout'; +import { useDocumentTitle } from '@rocket.chat/ui-client'; import { useTranslation } from 'react-i18next'; import type { DispatchLoginRouter } from './hooks/useLoginRouter'; const GuestForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRouter }) => { const { t } = useTranslation(); + useDocumentTitle(t('registration.component.login'), false); return (
diff --git a/packages/web-ui-registration/src/LoginForm.tsx b/packages/web-ui-registration/src/LoginForm.tsx index 35141c4646c7..1c6e0661104f 100644 --- a/packages/web-ui-registration/src/LoginForm.tsx +++ b/packages/web-ui-registration/src/LoginForm.tsx @@ -13,6 +13,7 @@ import { } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { Form, ActionLink } from '@rocket.chat/layout'; +import { useDocumentTitle } from '@rocket.chat/ui-client'; import { useLoginWithPassword, useSetting } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; import type { ReactElement } from 'react'; @@ -79,6 +80,8 @@ export const LoginForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRoute const usernameOrEmailPlaceholder = String(useSetting('Accounts_EmailOrUsernamePlaceholder')); const passwordPlaceholder = String(useSetting('Accounts_PasswordPlaceholder')); + useDocumentTitle(t('registration.component.login'), false); + const loginMutation = useMutation({ mutationFn: (formData: { username: string; password: string }) => { return login(formData.username, formData.password); diff --git a/packages/web-ui-registration/src/RegisterSecretPageRouter.tsx b/packages/web-ui-registration/src/RegisterSecretPageRouter.tsx index 889d7e53e7b9..08cf481061f9 100644 --- a/packages/web-ui-registration/src/RegisterSecretPageRouter.tsx +++ b/packages/web-ui-registration/src/RegisterSecretPageRouter.tsx @@ -1,5 +1,7 @@ +import { useDocumentTitle } from '@rocket.chat/ui-client'; import { useSetting } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; +import { useTranslation } from 'react-i18next'; import RegisterForm from './RegisterForm'; import RegisterFormDisabled from './RegisterFormDisabled'; @@ -16,12 +18,15 @@ export const RegisterSecretPageRouter = ({ setLoginRoute: DispatchLoginRouter; origin: 'register' | 'secret-register' | 'invite-register'; }): ReactElement => { + const { t } = useTranslation(); const registrationMode = useSetting('Accounts_RegistrationForm'); const isPublicRegistration = registrationMode === 'Public'; const isRegistrationAllowedForSecret = registrationMode === 'Secret URL'; const isRegistrationDisabled = registrationMode === 'Disabled' || (origin === 'register' && isRegistrationAllowedForSecret); + useDocumentTitle(t('registration.component.form.createAnAccount'), false); + if (origin === 'secret-register' && !isRegistrationAllowedForSecret) { return ; } diff --git a/packages/web-ui-registration/src/ResetPasswordForm.tsx b/packages/web-ui-registration/src/ResetPasswordForm.tsx index f395a093d4ec..ae771c4494f7 100644 --- a/packages/web-ui-registration/src/ResetPasswordForm.tsx +++ b/packages/web-ui-registration/src/ResetPasswordForm.tsx @@ -1,6 +1,7 @@ import { FieldGroup, TextInput, Field, FieldLabel, FieldRow, FieldError, ButtonGroup, Button, Callout } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { Form, ActionLink } from '@rocket.chat/layout'; +import { useDocumentTitle } from '@rocket.chat/ui-client'; import type { ReactElement } from 'react'; import { useEffect, useRef } from 'react'; import { useForm } from 'react-hook-form'; @@ -15,6 +16,8 @@ export const ResetPasswordForm = ({ setLoginRoute }: { setLoginRoute: DispatchLo const formLabelId = useUniqueId(); const forgotPasswordFormRef = useRef(null); + useDocumentTitle(t('registration.component.resetPassword'), false); + const { register, handleSubmit, From 88a871f64f9b38a9b169c72c3ad9d8b40fed1a1e Mon Sep 17 00:00:00 2001 From: "lingohub[bot]" <69908207+lingohub[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 20:41:48 +0000 Subject: [PATCH 9/9] =?UTF-8?q?i18n:=20Language=20update=20from=20LingoHub?= =?UTF-8?q?=20=F0=9F=A4=96=20on=202023-12-05Z=20(#31161)?= 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> --- .../rocketchat-i18n/i18n/af.i18n.json | 3 ++- .../rocketchat-i18n/i18n/ar.i18n.json | 9 ++----- .../rocketchat-i18n/i18n/ca.i18n.json | 9 ++----- .../rocketchat-i18n/i18n/de.i18n.json | 10 ++------ .../rocketchat-i18n/i18n/en.i18n.json | 2 +- .../rocketchat-i18n/i18n/es.i18n.json | 8 +------ .../rocketchat-i18n/i18n/fi.i18n.json | 24 ++++++++++++------- .../rocketchat-i18n/i18n/fr.i18n.json | 9 ++----- .../rocketchat-i18n/i18n/hu.i18n.json | 9 ++----- .../rocketchat-i18n/i18n/ja.i18n.json | 9 ++----- .../rocketchat-i18n/i18n/nl.i18n.json | 9 ++----- .../rocketchat-i18n/i18n/pl.i18n.json | 9 ++----- .../rocketchat-i18n/i18n/pt-BR.i18n.json | 9 ++----- .../rocketchat-i18n/i18n/ru.i18n.json | 9 ++----- .../rocketchat-i18n/i18n/sv.i18n.json | 10 ++------ .../rocketchat-i18n/i18n/zh-TW.i18n.json | 4 ++-- 16 files changed, 44 insertions(+), 98 deletions(-) diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/af.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/af.i18n.json index 428c82b3efbe..90db76e13637 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/af.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/af.i18n.json @@ -2757,5 +2757,6 @@ "registration.component.form.invalidConfirmPass": "Die wagwoord bevestiging pas nie by die wagwoord nie", "registration.component.form.confirmPassword": "Bevestig jou wagwoord", "registration.component.form.sendConfirmationEmail": "Stuur bevestiging e-pos", + "Enterprise": "onderneming", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json index 4c85ec6e3068..f4edf2fed22f 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json @@ -840,7 +840,6 @@ "Close": "إغلاق", "Close_chat": "إغلاق الدردشة", "Close_room_description": "إنك توشك على إغلاق هذه الدردشة. هل تريد فعلاً المتابعة؟", - "Close_to_seat_limit_warning": "لا يمكن إنشاء أعضاء جدد بمجرد استيفاء حد المقاعد.", "close-livechat-room": "إغلاق Room القناة متعددة الاتجاهات", "close-livechat-room_description": "إذن لإغلاق غرفة القناة متعددة الاتجاهات الحالية", "Close_menu": "إغلاق القائمة", @@ -3508,11 +3507,6 @@ "Report_this_message_question_mark": "هل أبلغ عن هذه الرسالة؟", "Reporting": "يتم الإبلاغ الآن", "Request": "طلب", - "Request_seats": "طلب مقاعد", - "Request_more_seats": "طلب المزيد من المقاعد.", - "Request_more_seats_out_of_seats": "لا يمكنك إضافة أعضاء لأن مساحة العمل هذه ليس بها مقاعد، يرجى طلب المزيد من المقاعد.", - "Request_more_seats_sales_team": "بمجرد تقديم طلبك، سينظر فريق المبيعات لدينا في الأمر وسيتواصل معك في غضون اليومين المقبلين.", - "Request_more_seats_title": "طلب المزيد من المقاعد", "Request_comment_when_closing_conversation": "طلب التعليق عند إغلاق المحادثة", "Request_comment_when_closing_conversation_description": "في حال التمكين، سيحتاج الوكيل إلى تعيين تعليق قبل إغلاق المحادثة.", "Request_tag_before_closing_chat": "طلب علامة (علامات) قبل إغلاق المحادثة", @@ -4889,6 +4883,7 @@ "RegisterWorkspace_Features_Omnichannel_Title": "قناة متعددة الاتجاهات", "RegisterWorkspace_Setup_Label": "البريد الإلكتروني لحساب السحابة", "cloud.RegisterWorkspace_Setup_Terms_Privacy": "أوافق على <1>البنود والشروط و<3>سياسة الخصوصية", + "Enterprise": "مؤسسة", "UpgradeToGetMore_engagement-dashboard_Title": "التحليلات", "UpgradeToGetMore_auditing_Title": "تدقيق الرسائل" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json index e2b1298bdea2..b56b0b645d6b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json @@ -835,7 +835,6 @@ "Close": "Tanca", "Close_chat": "Tancar xat", "Close_room_description": "Esteu a punt de tancar aquest xat. Esteu segur que voleu continuar?", - "Close_to_seat_limit_warning": "No es poden crear nous membres una vegada que s'arriba al límit de seients.", "close-livechat-room": "Tancar Room de Livechat", "close-livechat-room_description": "Permís per tancar la sala d'LiveChat actual", "Close_menu": "Tanca el menú", @@ -3454,11 +3453,6 @@ "Report_this_message_question_mark": "Informar d'aquest missatge?", "Reporting": "Informes", "Request": "Sol·licitud", - "Request_seats": "Sol.licitar llocs", - "Request_more_seats": "Sol·licita més llocs.", - "Request_more_seats_out_of_seats": "No podeu afegir membres perquè aquest espai de treball no té lloc, demani més llocs.", - "Request_more_seats_sales_team": "Una vegada que enviï la seva sol·licitud, el nostre equip de vendes l'analitzarà i es comunicarà amb vostè en els pròxims dies.", - "Request_more_seats_title": "Sol·licitar més llocs", "Request_comment_when_closing_conversation": "Sol·licitar un comentari al tancar la conversa", "Request_comment_when_closing_conversation_description": "Si està activat, l'agent haurà de fer un comentari abans que es tanqui la conversa.", "Request_tag_before_closing_chat": "Sol·licitar etiqueta(es) abans de tancar la conversa", @@ -4693,6 +4687,7 @@ "onboarding.form.registerOfflineForm.title": "Registra't sense connexió", "RegisterWorkspace_Features_Marketplace_Title": "Mercat", "RegisterWorkspace_Features_Omnichannel_Title": "LiveChat", + "Enterprise": "Empresa", "UpgradeToGetMore_engagement-dashboard_Title": "Analítiques", "UpgradeToGetMore_auditing_Title": "Auditoria de missatges" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json index 6601535bc40b..e8e385da9809 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json @@ -951,7 +951,6 @@ "Close": "Schließen", "Close_chat": "Chat schließen", "Close_room_description": "Sie sind im Begriff, diesen Chat zu schließen. Sind Sie sicher, dass Sie fortfahren möchten?", - "Close_to_seat_limit_warning": "Wenn das Platzlimit erreicht ist, können keine weiteren neuen Mitglieder erstellt werden.", "close-livechat-room": "Livechat-Room schließen", "close-livechat-room_description": "Berechtigung, den aktuellen Livechat-Raum zu schließen", "Close_menu": "Menü schließen", @@ -1479,7 +1478,6 @@ "Delete_message": "Nachricht löschen", "Delete_my_account": "Mein Konto löschen", "Delete_Role_Warning": "Wenn Sie eine Rolle löschen, wird sie für immer gelöscht. Dies kann nicht rückgängig gemacht werden.", - "Delete_Role_Warning_Community_Edition": "Dies kann nicht rückgängig gemacht werden. Beachten Sie, dass es in der Community Edition nicht möglich ist, neue benutzerdefinierte Rollen zu erstellen.", "Delete_Room_Warning": "Beim Löschen eines Raumes werden alle Nachrichten in diesem Raum unwiderruflich gelöscht.", "Delete_User_Warning": "Beim Löschen eines Benutzers werden alle Nachrichten des Benutzers unwiderruflich gelöscht.", "Delete_User_Warning_Delete": "Beim Löschen eines Benutzers werden alle Nachrichten des Benutzers unwiderruflich gelöscht.", @@ -3950,11 +3948,6 @@ "Report_this_message_question_mark": "Diese Nachricht melden?", "Reporting": "Berichtswesen", "Request": "Anfordern", - "Request_seats": "Plätze anfordern", - "Request_more_seats": "Mehr Plätze anfordern", - "Request_more_seats_out_of_seats": "Sie können keine Mitglieder hinzufügen, weil dieser Arbeitsplätze keine Plätze mehr hat, bitte fordern Sie mehr Plätze an.", - "Request_more_seats_sales_team": "Nachdem Sie Ihre Anforderung übergeben haben, wird sich unser Sales Team darum kümmern und sich in den nächsten Tagen bei Ihnen melden.", - "Request_more_seats_title": "Mehr Plätze anfordern", "Request_comment_when_closing_conversation": "Kommentar beim Schließen der Konversation anfordern", "Request_comment_when_closing_conversation_description": "Wenn dies aktiviert ist, muss der Agent einen Kommentar eingeben, bevor das Gespräch geschlossen wird.", "Request_tag_before_closing_chat": "Fordern Sie Tags an, bevor Sie die Unterhaltung beenden", @@ -5520,6 +5513,7 @@ "RegisterWorkspace_Connection_Error": "Beim Verbinden ist ein Fehler aufgetreten", "cloud.RegisterWorkspace_Setup_Terms_Privacy": "Ich bin mit den Nutzungsvereinbarung und den Datenschutzbestimmungen einverstanden", "Uninstall_grandfathered_app": "{{appName}} deinstallieren?", + "Enterprise": "Unternehmen", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "UpgradeToGetMore_auditing_Title": "Nachrichtenüberprüfung" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 33df4302103b..b9fc55199d35 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -6234,4 +6234,4 @@ "Seat_limit_reached": "Seat limit reached", "Seat_limit_reached_Description": "Your workspace reached its contractual seat limit. Buy more seats to add more users.", "Buy_more_seats": "Buy more seats" -} +} \ 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 31e0cba48a68..e732242050b8 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json @@ -847,7 +847,6 @@ "Close": "Cerrar", "Close_chat": "Cerrar chat", "Close_room_description": "Estás a punto de cerrar este chat. ¿Seguro que quieres continuar?", - "Close_to_seat_limit_warning": "No se pueden crear nuevos miembros una vez que se alcanza el límite de puestos.", "close-livechat-room": "Cerrar Room de Omnichannel", "close-livechat-room_description": "Permiso para cerrar la sala de Omnichannel actual", "Close_menu": "Cerrar menú", @@ -3497,11 +3496,6 @@ "Report_this_message_question_mark": "¿Denunciar este mensaje?", "Reporting": "Denuncia", "Request": "Solicitar", - "Request_seats": "Solicitar puestos", - "Request_more_seats": "Solicitar más puestos.", - "Request_more_seats_out_of_seats": "No puedes añadir miembros porque este espacio de trabajo se ha quedado sin puestos libres. Solicita más puestos.", - "Request_more_seats_sales_team": "Una vez que se envíe tu solicitud, nuestro equipo de ventas la analizará y contactará contigo en un par de días.", - "Request_more_seats_title": "Solicitar más puestos", "Request_comment_when_closing_conversation": "Solicitar comentario al cerrar la conversación", "Request_comment_when_closing_conversation_description": "Si esta opción está habilitada, el agente tendrá que escribir un comentario antes de que se cierre la conversación.", "Request_tag_before_closing_chat": "Solicitar etiquetas antes de cerrar la conversación", @@ -5088,4 +5082,4 @@ "Unlimited_seats": "Puestos ilimitados", "Unlimited_MACs": "Contactos Activos por Mes (MAC) ilimitados", "Unlimited_seats_MACs": "Puestos y Contactos Activos por Mes (MAC) ilimitados" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json index d05169441d02..33dff04a621e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json @@ -324,6 +324,7 @@ "Added__username__to_this_team": "lisäsi käyttäjän @{{user_added}} tähän tiimiin", "Adding_OAuth_Services": "Lisätään OAuth-palveluja", "Adding_permission": "Lisätään oikeutta", + "Adjustable_layout": "Säädettävä ulkoasu", "Adding_user": "Lisätään käyttäjää", "Additional_emails": "Lisäsähköposti", "Additional_Feedback": "Lisäpalaute", @@ -335,6 +336,8 @@ "admin-no-videoconf-provider-app": "**Neuvottelupuhelu ei ole käytössä**: Neuvottelupuhelusovelluksia on saatavilla Rocket.Chat-kaupassa.", "Administration": "Hallinta", "Address": "Osoite", + "Adjustable_font_size": "Säädettävä fonttikoko", + "Adjustable_font_size_description": "Suunniteltu niille, jotka pitävät suuremmasta tai pienemmästä tekstistä luettavuuden parantamiseksi. Tämä edistää inklusiivisuutta antamalla käyttäjille mahdollisuuden räätälöidä ohjelmiston käyttöliittymä omien tarpeidensa mukaan.", "Adult_images_are_not_allowed": "Aikuisviihdekuvat eivät ole sallittuja", "Aerospace_and_Defense": "Ilmailu ja puolustus", "After_OAuth2_authentication_users_will_be_redirected_to_this_URL": "OAuth2-todennuksen jälkeen käyttäjät ohjataan tässä luettelossa olevaan URL-osoitteeseen. Kullekin riville voi lisätä yhden URL-osoitteen.", @@ -967,7 +970,6 @@ "Close": "Sulje", "Close_chat": "Sulje keskustelu", "Close_room_description": "Olet sulkemassa keskustelun. Haluatko varmasti jatkaa?", - "Close_to_seat_limit_warning": "Uusia jäseniä ei voida luoda, kun käyttäjämäärä on saavutettu.", "close-livechat-room": "Sulje Omnichannel-huone", "close-livechat-room_description": "Lupa nykyisen monikanavahuoneen sulkemiseen", "Close_menu": "Sulje valikko", @@ -1494,7 +1496,6 @@ "Delete_message": "Poista viesti", "Delete_my_account": "Poista tilini", "Delete_Role_Warning": "Roolin poisto poistaa sen lopullisesti. Tätä ei voi kumota.", - "Delete_Role_Warning_Community_Edition": "Tätä ei voi kumota. Huomioi, että yhteisöversiossa ei voi luoda uusia mukautettuja rooleja", "Delete_Room_Warning": "Huoneen poisto poistaa kaikki huoneessa olevat viestit. Tätä ei voi kumota.", "Delete_User_Warning": "Käyttäjän poisto poistaa myös kaikki käyttäjän lähettämät viestit. Tätä ei voi kumota.", "Delete_User_Warning_Delete": "Käyttäjän poisto poistaa myös kaikki käyttäjän lähettämät viestit. Tätä ei voi kumota.", @@ -2261,6 +2262,7 @@ "Force_visitor_to_accept_data_processing_consent_enabled_alert": "Tietojenkäsittelyn hyväksynnän on perustuttava käsittelysyyn selkeään ymmärtämiseen. Määritä siksi alla oleva asetus, joka näkyy käyttäjille, jotta he tietävät, miksi keräät ja käsittelet henkilötietoja.", "force-delete-message": "Pakota viestin poisto", "force-delete-message_description": "Oikeus poistaa viesti ohittaen kaikki rajoitukset", + "Font_size": "Fonttikoko", "Forgot_password": "Unohditko salasanasi?", "Forgot_Password_Description": "Voit käyttää seuraavia paikkamerkkejä: \n - `[Forgot_Password_Url]` salasanan palautus-URL-osoitteen paikalla. \n - `[name]`, `[fname]`, `[lname]` käyttäjän koko nimen, etunimen tai sukunimen paikalla. \n - `[email]` käyttäjän sähköpostiosoitteen paikalla. \n - `[Site_Name]` ja `[Site_URL]` sovelluksen nimen ja URL-osoitteen paikalla.", "Forgot_Password_Email": "Nollaa salasanasi napsauttamalla tätä.", @@ -3199,6 +3201,8 @@ "Mentions": "Maininnat", "Mentions_default": "Maininnat (oletus)", "Mentions_only": "Vain maininnat", + "Mentions_with_@_symbol": "Maininnat @ merkillä", + "Mentions_with_@_symbol_description": "Maininnat huomauttavat käyttäjiä ja korostavat tietyille ryhmille tai käyttäjille tarkoitettuja viestejä, mikä helpottaa kohdennettua viestintää.\n\nNäytönlukuohjelman toiminnallisuus on optimoitu, kun \"@\"-symbolia käytetään mainintaominaisuuteen. Tämä varmistaa, että näytönlukuohjelmia käyttävät käyttäjät voivat helposti tulkita maininnat ja reagoida niihin.", "Merge_Channels": "Yhdistä kanavat Channel", "message": "viesti", "Message": "Viesti", @@ -4015,11 +4019,6 @@ "Report_this_message_question_mark": "Haluatko ilmoittaa tästä viestistä?", "Reporting": "Ilmoitetaan", "Request": "Pyyntö", - "Request_seats": "Pyydä paikkoja", - "Request_more_seats": "Pyydä lisää paikkoja.", - "Request_more_seats_out_of_seats": "Et voi lisätä jäseniä, koska tässä työtilassa ei ole enää paikkoja, pyydä lisää paikkoja.", - "Request_more_seats_sales_team": "Kun pyyntösi on lähetetty, myyntitiimimme selvittää sitä ja ottaa sinuun yhteyttä lähipäivinä.", - "Request_more_seats_title": "Pyydä lisää paikkoja", "Request_comment_when_closing_conversation": "Pyydä kommenttia keskustelun päättämisen yhteydessä", "Request_comment_when_closing_conversation_description": "Jos tämä on käytössä, agentin on määritettävä kommentti ennen keskustelun sulkemista.", "Request_tag_before_closing_chat": "Pyydä tunniste(et) ennen keskustelun päättämistä", @@ -4457,6 +4456,9 @@ "Showing_online_users": "Näytetään: {{total_showing}}, Online: {{online}}, Yhteensä: {{total}} käyttäjää", "Showing_results": "

Näytetään %s tulosta

", "Showing_results_of": "Näytetään tulokset %s - %s %s:stä", + "Show_usernames": "Näytä käyttäjänimet", + "Show_or_hide_the_user_roles_of_message_authors": "Näytä tai piilota viestin kirjoittajien käyttäjäroolit.", + "Show_or_hide_the_username_of_message_authors": "Näytä tai piilota viestin kirjoittajien käyttäjänimet.", "Sidebar": "Sivupalkki", "Sidebar_list_mode": "Sivupalkin kanavaluettelon tila", "Sign_in_to_start_talking": "Kirjaudu ja ala puhua", @@ -5637,7 +5639,9 @@ "Something_Went_Wrong": "Jokin meni pieleen", "Toolbox_room_actions": "Ensisijaiset huoneen Room toimet", "Theme_light": "Vaalea", + "Theme_light_description": "Helppokäyttöisempi vaihtoehto yksilöille, joilla on näönvaraisen hahmottamisen ongelmia. Hyvä valinta hyvin valaistuihin olosuhteisiin.", "Theme_dark": "Tumma", + "Theme_dark_description": "Vähennä silmien rasitusta ja väsymystä huonosti valaistuissa olosuhteissa minimoimalla näytön tuottamaa valoa.", "Enable_of_limit_apps_currently_enabled": "**{{enabled}}/{{limit}} {{context}} sovellusta on nyt käytössä.** \n \nYhteisöversion työtiloissa voi olla käytössä enintään {{limit}} {{context}}sovellusta. \n \n**{{appName}} on oletusarvoisesti poissa käytöstä.** Jos haluat ottaa tämän sovelluksen käyttöön, poista käytöstä jokin muu {{context}}sovellus tai päivitä yritysversioon.", "Enable_of_limit_apps_currently_enabled_exceeded": "**{{enabled}}/{{limit}} {{context}}sovellusta on nyt käytössä.** \n \nYhteisöversion sovellusraja on saavutettu. \n \nYhteisöversion työtiloissa voi olla käytössä enintään {{limit}} {{context}}sovellusta. \n \n**{{appName}} poistetaan oletusarvoisesti käytöstä.** Poista käytöstä vähintään {{exceed}} muuta {{context}}sovellusta tai päivitä yritysversioon, jotta voit ottaa tämän sovelluksen käyttöön.", "Workspaces_on_Community_edition_install_app": "Yhteisöversion työtiloissa voi olla käytössä enintään {{limit}} {{context}}sovellusta. Päivitä yritysversioon, jotta voit ottaa sovelluksia käyttöön rajattomasti.", @@ -5651,6 +5655,9 @@ "Disable_at_least_more_apps": "Poista käytöstä vähintään {{numberOfExceededApps}} muuta sovellusta tai päivitä yritysversioon, jotta voit ottaa tämän sovelluksen käyttöön.", "Community_Private_apps_limit_exceeded": "Yhteisöversion sovellusraja on ylitetty.", "Theme_match_system": "Järjestelmän mukaan", + "Theme_match_system_description": "Seuraa käyttöjärjestelmän teemaa.", + "Theme_high_contrast": "Korkea kontrasti", + "Theme_high_contrast_description": "Maksimaalinen sävyerottelu korostetuilla väreillä ja terävillä kontrasteilla parantaa käytettävyyttä.", "Join_your_team": "Liity tiimiisi", "Create_an_account": "Luo tili", "Get_all_apps": "Hanki kaikki sovellukset, jotka tiimisi tarvitsee", @@ -5745,5 +5752,6 @@ "Uninstall_grandfathered_app": "Poistetaanko {{appName}}?", "App_will_lose_grandfathered_status": "**Tämä {{context}}sovellus menettää aikaisemmin käytetössä olleen sovelluksen tilansa.** \n \nYhteisöversion työtiloissa voi olla käytössä enintään {{limit}} {{context}} sovellusta. aikaisemmin Aikaisemmin käytössä olleet sovellukset lasketaan mukaan rajoitukseen, mutta rajoitusta ei sovelleta niihin.", "Theme_Appearence": "Teeman ulkoasu", + "Enterprise": "Yritys", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json index 658957adf4e5..fd7f87718e8c 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json @@ -843,7 +843,6 @@ "Close": "Fermer", "Close_chat": "Fermer le chat", "Close_room_description": "Vous êtes sur le point de fermer ce chat. Voulez-vous continuer ?", - "Close_to_seat_limit_warning": "Aucun nouveau membre ne peut être créé une fois que la limite de sièges est atteinte.", "close-livechat-room": "Fermer le salon omnicanal", "close-livechat-room_description": "Autorisation de fermer le salon omnicanal actuel", "Close_menu": "Fermer le menu", @@ -3502,11 +3501,6 @@ "Report_this_message_question_mark": "Signaler ce message ?", "Reporting": "Rapports", "Request": "Demande", - "Request_seats": "Demander des sièges", - "Request_more_seats": "Demander plus de sièges.", - "Request_more_seats_out_of_seats": "Vous ne pouvez pas ajouter de membres car cet espace de travail n'a plus de sièges, demandez plus de sièges.", - "Request_more_seats_sales_team": "Une fois votre demande soumise, notre équipe commerciale l'examinera et vous contactera dans les prochains jours.", - "Request_more_seats_title": "Demander plus de sièges", "Request_comment_when_closing_conversation": "Demander un commentaire lors de la fermeture de la conversation", "Request_comment_when_closing_conversation_description": "Si cette option est activée, l'agent devra définir un commentaire avant la fermeture de la conversation.", "Request_tag_before_closing_chat": "Demander des balises avant de fermer la conversation", @@ -4889,6 +4883,7 @@ "RegisterWorkspace_Features_Omnichannel_Title": "Omnicanal", "RegisterWorkspace_Setup_Label": "E-mail du compte cloud", "cloud.RegisterWorkspace_Setup_Terms_Privacy": "J'accepte les <1>Conditions d'utilisation et la <3>Politique de confidentialité", + "Enterprise": "Entreprise", "UpgradeToGetMore_engagement-dashboard_Title": "Analyses", "UpgradeToGetMore_auditing_Title": "Audit des messages" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json index 8c2a2f411e3d..31bb9653e677 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json @@ -935,7 +935,6 @@ "Close": "Bezárás", "Close_chat": "Csevegés lezárása", "Close_room_description": "A csevegés lezárására készül. Biztosan folytatni szeretné?", - "Close_to_seat_limit_warning": "Nem lehet új tagokat létrehozni, ha a helyek elérték a korlátot.", "close-livechat-room": "Összcsatornás szoba lezárása", "close-livechat-room_description": "Jogosultság a jelenlegi összcsatornás szoba lezárásához", "Close_menu": "Menü bezárása", @@ -3872,11 +3871,6 @@ "Report_this_message_question_mark": "Jelenti ezt az üzenetet?", "Reporting": "Jelentés", "Request": "Kérés", - "Request_seats": "Helyek kérése", - "Request_more_seats": "További helyek kérése.", - "Request_more_seats_out_of_seats": "Nem tud tagokat hozzáadni, mert ezen a munkaterületen elfogytak a helyek. Kérjen további helyeket.", - "Request_more_seats_sales_team": "Miután a kérése elküldésre került, az értékesítési csapatunk megvizsgálja azt, és felveszi Önnel a kapcsolatot a következő néhány napon belül.", - "Request_more_seats_title": "További helyek kérése", "Request_comment_when_closing_conversation": "Megjegyzés kérése a beszélgetés lezárásakor", "Request_comment_when_closing_conversation_description": "Ha engedélyezve van, akkor az ügyintézőnek be kell állítania egy megjegyzést a beszélgetés lezárása előtt.", "Request_tag_before_closing_chat": "Címkék kérése a beszélgetés lezárása előtt", @@ -5428,6 +5422,7 @@ "Join_your_team": "Csatlakozás csapathoz", "Create_an_account": "Fiók létrehozása", "RegisterWorkspace_Features_Marketplace_Title": "Piactér", + "Enterprise": "Vállalati", "UpgradeToGetMore_engagement-dashboard_Title": "Analitika", "UpgradeToGetMore_auditing_Title": "Üzenet ellenőrzés" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json index 17dd168848c7..3d897682d64c 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json @@ -830,7 +830,6 @@ "Close": "閉じる", "Close_chat": "チャットを閉じる", "Close_room_description": "このチャットを閉じようとしています。続行してよろしいですか?", - "Close_to_seat_limit_warning": "シートの上限に達すると、新しいメンバーを作成できません。", "close-livechat-room": "オムニチャネルRoomを閉じる", "close-livechat-room_description": "現在のオムニチャネルルームを閉じる権限", "Close_menu": "閉じる", @@ -3472,11 +3471,6 @@ "Report_this_message_question_mark": "このメッセージを報告しますか?", "Reporting": "報告", "Request": "要求", - "Request_seats": "シートの要求", - "Request_more_seats": "追加のシートを要求します。", - "Request_more_seats_out_of_seats": "このワークスペースはシートが不足しているため、メンバーを追加できません。追加のシートを要求してください。", - "Request_more_seats_sales_team": "要求が送信されました。確認後、営業チームから数日以内にご連絡いたします。", - "Request_more_seats_title": "追加のシートの要求", "Request_comment_when_closing_conversation": "会話を閉じるときにコメントを要求", "Request_comment_when_closing_conversation_description": "有効にした場合、エージェントは会話が閉じられる前にコメントを設定する必要があります。", "Request_tag_before_closing_chat": "会話を閉じる前にタグを要求", @@ -4839,6 +4833,7 @@ "RegisterWorkspace_Features_Omnichannel_Title": "オムニチャネル", "RegisterWorkspace_Setup_Label": "クラウドアカウントメール", "cloud.RegisterWorkspace_Setup_Terms_Privacy": "<1>使用と<3>プライバシーポリシーに同意します", + "Enterprise": "エンタープライズ", "UpgradeToGetMore_engagement-dashboard_Title": "分析", "UpgradeToGetMore_auditing_Title": "メッセージ監査" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json index 3ed08591c2dc..79e780d6520d 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json @@ -838,7 +838,6 @@ "Close": "Sluiten", "Close_chat": "Chat sluiten", "Close_room_description": "Je staat op het punt deze chat te sluiten. Weet je zeker dat je wilt doorgaan?", - "Close_to_seat_limit_warning": "Er kunnen geen nieuwe leden worden aangemaakt zodra de plaatslimiet bereikt is.", "close-livechat-room": "Omnichannel-ruimte sluiten", "close-livechat-room_description": "Toestemming om de huidige omnichannel-ruimte te sluiten", "Close_menu": "Menu sluiten", @@ -3495,11 +3494,6 @@ "Report_this_message_question_mark": "Dit bericht melden?", "Reporting": "Rapporteren", "Request": "Verzoek", - "Request_seats": "Plaatsen aanvragen", - "Request_more_seats": "Meer plaatsen aanvragen.", - "Request_more_seats_out_of_seats": "Je kan geen leden toevoegen omdat deze werkruimte geen plaatsen meer heeft. Vraag meer plaatsen aan.", - "Request_more_seats_sales_team": "Zodra uw verzoek is ingediend, zal ons verkoopteam het bekijken en binnen enkele dagen contact met u opnemen.", - "Request_more_seats_title": "Meer plaatsen aanvragen", "Request_comment_when_closing_conversation": "Commentaar vragen bij het sluiten van het gesprek", "Request_comment_when_closing_conversation_description": "Indien ingeschakeld, moet de agent een opmerking plaatsen voor het gesprek wordt gesloten.", "Request_tag_before_closing_chat": "Vraag tag(s) aan voordat u het gesprek sluit", @@ -4877,6 +4871,7 @@ "RegisterWorkspace_Features_Omnichannel_Title": "Omnichannel", "RegisterWorkspace_Setup_Label": "E-mailadres van cloudaccount", "cloud.RegisterWorkspace_Setup_Terms_Privacy": "Ik ga akkoord met de <1>Algemene voorwaarden en <3>Privacybeleid", + "Enterprise": "Onderneming", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "UpgradeToGetMore_auditing_Title": "Bericht auditing" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json index ef47a637cd28..1906edf4708a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json @@ -917,7 +917,6 @@ "Close": "Zamknij", "Close_chat": "Zamknij czat", "Close_room_description": "Za chwilę zamkniesz ten czat. Czy na pewno chcesz kontynuować?", - "Close_to_seat_limit_warning": "Po wyczerpaniu limitu miejsc nie można tworzyć nowych członków.", "close-livechat-room": "Zamknij pokój Omnichannel", "close-livechat-room_description": "Uprawnienie do zamykania bieżącego pokoju Omnichannel", "Close_menu": "Zamknij menu", @@ -3804,11 +3803,6 @@ "Report_this_message_question_mark": "Zgłoś tą wiadomość?", "Reporting": "Raportowanie", "Request": "Rządanie", - "Request_seats": "Zamówienie miejsc", - "Request_more_seats": "Zamów więcej miejsc", - "Request_more_seats_out_of_seats": "Nie możesz dodać członków, ponieważ ta przestrzeń robocza nie ma wolnych miejsc, poproś o więcej miejsc.", - "Request_more_seats_sales_team": "Po złożeniu wniosku, nasz Zespół Sprzedaży zajmie się nim i skontaktuje się z Tobą w ciągu kilku najbliższych dni.", - "Request_more_seats_title": "Zamów więcej miejsc", "Request_comment_when_closing_conversation": "Poproś o komentarz podczas zamykania rozmowy", "Request_comment_when_closing_conversation_description": "Jeśli jest włączona, agent będzie musiał ustawić komentarz przed zamknięciem rozmowy.", "Request_tag_before_closing_chat": "Poproś o znacznik (znaczniki) przed zamknięciem rozmowy", @@ -5348,6 +5342,7 @@ "RegisterWorkspace_Setup_Label": "E-mail konta w chmurze", "RegisterWorkspace_Syncing_Complete": "Synchronizacja zakończona", "cloud.RegisterWorkspace_Setup_Terms_Privacy": "Zgadzam się z <1>zasadami i warunkami i <3>Polityką prywatności.", + "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "Analityka", "UpgradeToGetMore_auditing_Title": "Audyt wiadomości" -} +} \ No newline at end of file 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 f6811d9d13d8..fd2dc66f738a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -880,7 +880,6 @@ "Close": "Fechar", "Close_chat": "Fechar conversa", "Close_room_description": "Você está prestes a fechar esta conversa. Você tem certeza de que deseja continuar?", - "Close_to_seat_limit_warning": "Novos membros não podem ser criados, pois o limite de lugares foi atingido.", "close-livechat-room": "Fechar sala omnichannel", "close-livechat-room_description": "Permissão para fechar a sala Omnichannel atual", "Close_menu": "Fechar menu", @@ -3567,11 +3566,6 @@ "Report_this_message_question_mark": "Denunciar esta mensagem?", "Reporting": "Relatórios", "Request": "Solicitar", - "Request_seats": "Solicitar lugares", - "Request_more_seats": "Solicitar mais lugares.", - "Request_more_seats_out_of_seats": "Você não pode adicionar membros porque este espaço de trabalho está sem lugares; solicite mais lugares.", - "Request_more_seats_sales_team": "Assim que sua solicitação for enviada, nossa equipe de vendas vai analisá-la e entrará em contato nos próximos dias.", - "Request_more_seats_title": "Solicitar mais lugares", "Request_comment_when_closing_conversation": "Solicitar comentário ao encerrar a conversa", "Request_comment_when_closing_conversation_description": "Se ativado, o agente precisará informar um comentário antes que a conversa seja encerrada.", "Request_tag_before_closing_chat": "Solicitar tag(s) antes de encerrar a conversa", @@ -4982,6 +4976,7 @@ "RegisterWorkspace_Features_Omnichannel_Title": "Omnichannel", "RegisterWorkspace_Setup_Label": "E-mail da conta da nuvem", "cloud.RegisterWorkspace_Setup_Terms_Privacy": "Eu concordo com os <1>Termos e condições e a <3>Política de privacidade", + "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "UpgradeToGetMore_auditing_Title": "Auditoria de mensagem" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json index d5c4d9bac281..46702496d0d2 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json @@ -939,7 +939,6 @@ "Close": "Закрыть", "Close_chat": "Закрыть чат", "Close_room_description": "Вы собираетесь закрыть этот чат. Вы уверены, что хотите продолжить?", - "Close_to_seat_limit_warning": "Создание новых участников невозможно после достижения ограничения на количество мест.", "close-livechat-room": "Закрыть комнату Livechat ", "close-livechat-room_description": "Разрешение на закрытие текущего чата LiveChat", "Close_menu": "Закрыть меню", @@ -3654,11 +3653,6 @@ "Report_this_message_question_mark": "Сообщить об этом сообщении?", "Reporting": "Сбор статистики", "Request": "Запрос", - "Request_seats": "Запросить рабочие места", - "Request_more_seats": "Запросите дополнительные рабочие места.", - "Request_more_seats_out_of_seats": "Вы не сможете добавить участников, так как в этом рабочем пространстве закончились места. Запросите дополнительные рабочие места.", - "Request_more_seats_sales_team": "После отправки запроса наши специалисты из отдела продаж рассмотрят его и свяжутся с вами в течение следующих нескольких дней.", - "Request_more_seats_title": "Запросить дополнительные рабочие места", "Request_comment_when_closing_conversation": "Запросить комментарий при закрытии разговора", "Request_comment_when_closing_conversation_description": "Если эта функция включена, то перед закрытием разговора агенту необходимо будет задать комментарий.", "Request_tag_before_closing_chat": "Запросить метки до закрытия разговора", @@ -5091,6 +5085,7 @@ "RegisterWorkspace_Setup_Label": "Адрес электронной почты учетной записи в облаке", "cloud.RegisterWorkspace_Setup_Terms_Privacy": "Я принимаю <1>Положения и условия и <3>Политику конфиденциальности", "Theme_Appearence": "Внешний вид", + "Enterprise": "Корпорация", "UpgradeToGetMore_engagement-dashboard_Title": "Аналитика", "UpgradeToGetMore_auditing_Title": "Аудит сообщений" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json index b01bb2180132..a4a86f4d1542 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json @@ -967,7 +967,6 @@ "Close": "Stäng", "Close_chat": "Stäng chatt", "Close_room_description": "Du håller på att stänga chatten. Vill du fortsätta?", - "Close_to_seat_limit_warning": "Du kan inte lägga till fler användare när gränsen för användare har uppnåtts.", "close-livechat-room": "Stäng Omnichannel-rum \n", "close-livechat-room_description": "Behörighet att stänga den aktuella Omnichannel-kanalen", "Close_menu": "Stäng meny", @@ -1496,7 +1495,6 @@ "Delete_message": "Ta bort meddelande", "Delete_my_account": "Radera mitt konto", "Delete_Role_Warning": "När du tar bort en roll tas den bort permanent. Du kan inte ångra åtgärden.", - "Delete_Role_Warning_Community_Edition": "Detta kan inte göras ogjort. Observera att det inte är möjligt att skapa nya anpassade roller i Community Edition", "Delete_Room_Warning": "Om man raderar ett rum, raderas alla postade meddelanden i det rummet. Denna åtgärd kan inte ångras.", "Delete_User_Warning": "Radering av en användare kommer att radera alla meddelanden från den användaren. Detta kan inte ångras.", "Delete_User_Warning_Delete": "Radering av en användare kommer att radera alla meddelanden från den användaren. Detta kan inte ångras.", @@ -4026,11 +4024,6 @@ "Report_this_message_question_mark": "Rapportera det här meddelandet?", "Reporting": "Rapportering", "Request": "Förfrågan", - "Request_seats": "Begär användarlicenser", - "Request_more_seats": "Begär fler användarlicenser", - "Request_more_seats_out_of_seats": "Du kan inte lägga till medlemmar eftersom alla arbetsytans användarlicenser används. Begär fler användarlicenser.", - "Request_more_seats_sales_team": "När din förfrågan har skickats tittar vårt säljteam på den och hör av sig till dig inom några dagar.", - "Request_more_seats_title": "Begär fler användarlicenser", "Request_comment_when_closing_conversation": "Begär en kommentar när konversationen avslutas", "Request_comment_when_closing_conversation_description": "Om alternativet är aktiverat måste agenten ange en kommentar innan konversationen avslutas.", "Request_tag_before_closing_chat": "Begär taggar innan konversationen avslutas", @@ -5756,5 +5749,6 @@ "Uninstall_grandfathered_app": "Avinstallera {{appName}}?", "App_will_lose_grandfathered_status": "**Denna {{context}}-app kommer att förlora sin status som gammal app.** \n \nArbetsytorna i Community Edition kan ha upp till {{limit}} __kontext__-appar aktiverade. Gamla appar inkluderas i gränsen, men gränsen tillämpas inte på dem.", "Theme_Appearence": "Utseende för tema", + "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} +} \ No newline at end of file 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 ceba93304213..e0d34d8edfc8 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json @@ -829,7 +829,6 @@ "Close": "關閉", "Close_chat": "關閉聊天", "Close_room_description": "您即將關閉此聊天。您確定要繼續嗎?", - "Close_to_seat_limit_warning": "一旦到達座位限制,則無法建立新會員。", "close-livechat-room": "關閉客服 Room", "close-livechat-room_description": "有權限關閉當前客服聊天室", "Close_menu": "關閉選單", @@ -4588,6 +4587,7 @@ "RegisterWorkspace_Features_Omnichannel_Title": "Omnichannel", "RegisterWorkspace_Setup_Label": "雲端帳戶電子郵件", "cloud.RegisterWorkspace_Setup_Terms_Privacy": "我同意<1>條款及條件和<3>隱私權政策", + "Enterprise": "企業", "UpgradeToGetMore_engagement-dashboard_Title": "分析", "UpgradeToGetMore_auditing_Title": "訊息稽核" -} +} \ No newline at end of file