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"