From 03cc25eec06eaa20ded1f8a306d3fc8babe6cd53 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 11 Sep 2024 17:07:02 +1000 Subject: [PATCH] Fix authenticated media download (#1947) * remove dead function * fix media download in room timeline * authenticate remaining media endpoints --- .../components/image-viewer/ImageViewer.tsx | 6 +- .../message/content/AudioContent.tsx | 19 +- .../message/content/FileContent.tsx | 42 ++-- .../message/content/ImageContent.tsx | 17 +- .../message/content/ThumbnailContent.tsx | 22 +- .../message/content/VideoContent.tsx | 21 +- src/app/components/message/content/util.ts | 30 --- src/app/features/room/RoomTimeline.tsx | 59 ++--- src/app/molecules/image-pack/ImagePack.jsx | 216 ++++++++++-------- src/app/organisms/invite-user/InviteUser.jsx | 12 +- .../profile-editor/ProfileEditor.jsx | 24 +- .../profile-viewer/ProfileViewer.jsx | 6 +- src/app/utils/matrix.ts | 17 ++ 13 files changed, 284 insertions(+), 207 deletions(-) delete mode 100644 src/app/components/message/content/util.ts diff --git a/src/app/components/image-viewer/ImageViewer.tsx b/src/app/components/image-viewer/ImageViewer.tsx index 4fd06b7a..4956a7b6 100644 --- a/src/app/components/image-viewer/ImageViewer.tsx +++ b/src/app/components/image-viewer/ImageViewer.tsx @@ -6,6 +6,7 @@ import { Box, Chip, Header, Icon, IconButton, Icons, Text, as } from 'folds'; import * as css from './ImageViewer.css'; import { useZoom } from '../../hooks/useZoom'; import { usePan } from '../../hooks/usePan'; +import { downloadMedia } from '../../utils/matrix'; export type ImageViewerProps = { alt: string; @@ -18,8 +19,9 @@ export const ImageViewer = as<'div', ImageViewerProps>( const { zoom, zoomIn, zoomOut, setZoom } = useZoom(0.2); const { pan, cursor, onMouseDown } = usePan(zoom !== 1); - const handleDownload = () => { - FileSaver.saveAs(src, alt); + const handleDownload = async () => { + const fileContent = await downloadMedia(src); + FileSaver.saveAs(fileContent, alt); }; return ( diff --git a/src/app/components/message/content/AudioContent.tsx b/src/app/components/message/content/AudioContent.tsx index d6a82a3b..71551b12 100644 --- a/src/app/components/message/content/AudioContent.tsx +++ b/src/app/components/message/content/AudioContent.tsx @@ -5,7 +5,6 @@ import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment'; import { Range } from 'react-range'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; -import { getFileSrcUrl } from './util'; import { IAudioInfo } from '../../../../types/matrix/common'; import { PlayTimeCallback, @@ -17,7 +16,12 @@ import { } from '../../../hooks/media'; import { useThrottle } from '../../../hooks/useThrottle'; import { secondsToMinutesAndSeconds } from '../../../utils/common'; -import { mxcUrlToHttp } from '../../../utils/matrix'; +import { + decryptFile, + downloadEncryptedMedia, + downloadMedia, + mxcUrlToHttp, +} from '../../../utils/matrix'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; const PLAY_TIME_THROTTLE_OPS = { @@ -49,10 +53,13 @@ export function AudioContent({ const useAuthentication = useMediaAuthentication(); const [srcState, loadSrc] = useAsyncCallback( - useCallback( - () => getFileSrcUrl(mxcUrlToHttp(mx, url, useAuthentication) ?? '', mimeType, encInfo, true), - [mx, url, useAuthentication, mimeType, encInfo] - ) + useCallback(async () => { + const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; + const fileContent = encInfo + ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo)) + : await downloadMedia(mediaUrl); + return URL.createObjectURL(fileContent); + }, [mx, url, useAuthentication, mimeType, encInfo]) ); const audioRef = useRef(null); diff --git a/src/app/components/message/content/FileContent.tsx b/src/app/components/message/content/FileContent.tsx index 62b8c56d..c866dab5 100644 --- a/src/app/components/message/content/FileContent.tsx +++ b/src/app/components/message/content/FileContent.tsx @@ -20,7 +20,6 @@ import FocusTrap from 'focus-trap-react'; import { IFileInfo } from '../../../../types/matrix/common'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; -import { getFileSrcUrl, getSrcFile } from './util'; import { bytesToSize } from '../../../utils/common'; import { READABLE_EXT_TO_MIME_TYPE, @@ -30,7 +29,12 @@ import { } from '../../../utils/mimeTypes'; import * as css from './style.css'; import { stopPropagation } from '../../../utils/keyboard'; -import { mxcUrlToHttp } from '../../../utils/matrix'; +import { + decryptFile, + downloadEncryptedMedia, + downloadMedia, + mxcUrlToHttp, +} from '../../../utils/matrix'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; const renderErrorButton = (retry: () => void, text: string) => ( @@ -80,19 +84,17 @@ export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: Rea const useAuthentication = useMediaAuthentication(); const [textViewer, setTextViewer] = useState(false); - const loadSrc = useCallback( - () => getFileSrcUrl(mxcUrlToHttp(mx, url, useAuthentication) ?? '', mimeType, encInfo), - [mx, url, useAuthentication, mimeType, encInfo] - ); - const [textState, loadText] = useAsyncCallback( useCallback(async () => { - const src = await loadSrc(); - const blob = await getSrcFile(src); - const text = blob.text(); + const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; + const fileContent = encInfo + ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo)) + : await downloadMedia(mediaUrl); + + const text = fileContent.text(); setTextViewer(true); return text; - }, [loadSrc]) + }, [mx, useAuthentication, mimeType, encInfo, url]) ); return ( @@ -174,9 +176,12 @@ export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: Read const [pdfState, loadPdf] = useAsyncCallback( useCallback(async () => { - const httpUrl = await getFileSrcUrl(mxcUrlToHttp(mx, url, useAuthentication) ?? '', mimeType, encInfo); + const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; + const fileContent = encInfo + ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo)) + : await downloadMedia(mediaUrl); setPdfViewer(true); - return httpUrl; + return URL.createObjectURL(fileContent); }, [mx, url, useAuthentication, mimeType, encInfo]) ); @@ -248,9 +253,14 @@ export function DownloadFile({ body, mimeType, url, info, encInfo }: DownloadFil const [downloadState, download] = useAsyncCallback( useCallback(async () => { - const httpUrl = await getFileSrcUrl(mxcUrlToHttp(mx, url, useAuthentication) ?? '', mimeType, encInfo); - FileSaver.saveAs(httpUrl, body); - return httpUrl; + const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; + const fileContent = encInfo + ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo)) + : await downloadMedia(mediaUrl); + + const fileURL = URL.createObjectURL(fileContent); + FileSaver.saveAs(fileURL, body); + return fileURL; }, [mx, url, useAuthentication, mimeType, encInfo, body]) ); diff --git a/src/app/components/message/content/ImageContent.tsx b/src/app/components/message/content/ImageContent.tsx index d0a31c39..0d1d6d3a 100644 --- a/src/app/components/message/content/ImageContent.tsx +++ b/src/app/components/message/content/ImageContent.tsx @@ -22,12 +22,11 @@ import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment'; import { IImageInfo, MATRIX_BLUR_HASH_PROPERTY_NAME } from '../../../../types/matrix/common'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; -import { getFileSrcUrl } from './util'; import * as css from './style.css'; import { bytesToSize } from '../../../utils/common'; import { FALLBACK_MIMETYPE } from '../../../utils/mimeTypes'; import { stopPropagation } from '../../../utils/keyboard'; -import { mxcUrlToHttp } from '../../../utils/matrix'; +import { decryptFile, downloadEncryptedMedia, mxcUrlToHttp } from '../../../utils/matrix'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; type RenderViewerProps = { @@ -79,10 +78,16 @@ export const ImageContent = as<'div', ImageContentProps>( const [viewer, setViewer] = useState(false); const [srcState, loadSrc] = useAsyncCallback( - useCallback( - () => getFileSrcUrl(mxcUrlToHttp(mx, url, useAuthentication) ?? '', mimeType || FALLBACK_MIMETYPE, encInfo), - [mx, url, useAuthentication, mimeType, encInfo] - ) + useCallback(async () => { + const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; + if (encInfo) { + const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) => + decryptFile(encBuf, mimeType ?? FALLBACK_MIMETYPE, encInfo) + ); + return URL.createObjectURL(fileContent); + } + return mediaUrl; + }, [mx, url, useAuthentication, mimeType, encInfo]) ); const handleLoad = () => { diff --git a/src/app/components/message/content/ThumbnailContent.tsx b/src/app/components/message/content/ThumbnailContent.tsx index b667e57e..0746137c 100644 --- a/src/app/components/message/content/ThumbnailContent.tsx +++ b/src/app/components/message/content/ThumbnailContent.tsx @@ -2,9 +2,9 @@ import { ReactNode, useCallback, useEffect } from 'react'; import { IThumbnailContent } from '../../../../types/matrix/common'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; -import { getFileSrcUrl } from './util'; -import { mxcUrlToHttp } from '../../../utils/matrix'; +import { decryptFile, downloadEncryptedMedia, mxcUrlToHttp } from '../../../utils/matrix'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; +import { FALLBACK_MIMETYPE } from '../../../utils/mimeTypes'; export type ThumbnailContentProps = { info: IThumbnailContent; @@ -15,17 +15,23 @@ export function ThumbnailContent({ info, renderImage }: ThumbnailContentProps) { const useAuthentication = useMediaAuthentication(); const [thumbSrcState, loadThumbSrc] = useAsyncCallback( - useCallback(() => { + useCallback(async () => { const thumbInfo = info.thumbnail_info; const thumbMxcUrl = info.thumbnail_file?.url ?? info.thumbnail_url; + const encInfo = info.thumbnail_file; if (typeof thumbMxcUrl !== 'string' || typeof thumbInfo?.mimetype !== 'string') { throw new Error('Failed to load thumbnail'); } - return getFileSrcUrl( - mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication) ?? '', - thumbInfo.mimetype, - info.thumbnail_file - ); + + const mediaUrl = mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication) ?? thumbMxcUrl; + if (encInfo) { + const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) => + decryptFile(encBuf, thumbInfo.mimetype ?? FALLBACK_MIMETYPE, encInfo) + ); + return URL.createObjectURL(fileContent); + } + + return mediaUrl; }, [mx, info, useAuthentication]) ); diff --git a/src/app/components/message/content/VideoContent.tsx b/src/app/components/message/content/VideoContent.tsx index 1683075b..f6ddbb5a 100644 --- a/src/app/components/message/content/VideoContent.tsx +++ b/src/app/components/message/content/VideoContent.tsx @@ -22,10 +22,14 @@ import { import * as css from './style.css'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; -import { getFileSrcUrl } from './util'; import { bytesToSize } from '../../../../util/common'; import { millisecondsToMinutesAndSeconds } from '../../../utils/common'; -import { mxcUrlToHttp } from '../../../utils/matrix'; +import { + decryptFile, + downloadEncryptedMedia, + downloadMedia, + mxcUrlToHttp, +} from '../../../utils/matrix'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; type RenderVideoProps = { @@ -70,10 +74,15 @@ export const VideoContent = as<'div', VideoContentProps>( const [error, setError] = useState(false); const [srcState, loadSrc] = useAsyncCallback( - useCallback( - () => getFileSrcUrl(mxcUrlToHttp(mx, url, useAuthentication) ?? '', mimeType, encInfo, true), - [mx, url, useAuthentication, mimeType, encInfo] - ) + useCallback(async () => { + const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; + const fileContent = encInfo + ? await downloadEncryptedMedia(mediaUrl, (encBuf) => + decryptFile(encBuf, mimeType, encInfo) + ) + : await downloadMedia(mediaUrl); + return URL.createObjectURL(fileContent); + }, [mx, url, useAuthentication, mimeType, encInfo]) ); const handleLoad = () => { diff --git a/src/app/components/message/content/util.ts b/src/app/components/message/content/util.ts deleted file mode 100644 index 8614b8ec..00000000 --- a/src/app/components/message/content/util.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment'; -import { decryptFile } from '../../../utils/matrix'; - -export const getFileSrcUrl = async ( - httpUrl: string, - mimeType: string, - encInfo?: EncryptedAttachmentInfo, - forceFetch?: boolean -): Promise => { - if (encInfo) { - if (typeof httpUrl !== 'string') throw new Error('Malformed event'); - const encRes = await fetch(httpUrl, { method: 'GET' }); - const encData = await encRes.arrayBuffer(); - const decryptedBlob = await decryptFile(encData, mimeType, encInfo); - return URL.createObjectURL(decryptedBlob); - } - if (forceFetch) { - const res = await fetch(httpUrl, { method: 'GET' }); - const blob = await res.blob(); - return URL.createObjectURL(blob); - } - - return httpUrl; -}; - -export const getSrcFile = async (src: string): Promise => { - const res = await fetch(src, { method: 'GET' }); - const blob = await res.blob(); - return blob; -}; diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 34f54868..a2738fcb 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -17,7 +17,6 @@ import { EventTimelineSet, EventTimelineSetHandlerMap, IContent, - IEncryptedFile, MatrixClient, MatrixEvent, Room, @@ -48,12 +47,7 @@ import { import { isKeyHotkey } from 'is-hotkey'; import { Opts as LinkifyOpts } from 'linkifyjs'; import { useTranslation } from 'react-i18next'; -import { - decryptFile, - eventWithShortcode, - factoryEventSentBy, - getMxIdLocalPart, -} from '../../utils/matrix'; +import { eventWithShortcode, factoryEventSentBy, getMxIdLocalPart } from '../../utils/matrix'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useVirtualPaginator, ItemRange } from '../../hooks/useVirtualPaginator'; import { useAlive } from '../../hooks/useAlive'; @@ -220,18 +214,6 @@ export const getEventIdAbsoluteIndex = ( return baseIndex + eventIndex; }; -export const factoryGetFileSrcUrl = - (httpUrl: string, mimeType: string, encFile?: IEncryptedFile) => async (): Promise => { - if (encFile) { - if (typeof httpUrl !== 'string') throw new Error('Malformed event'); - const encRes = await fetch(httpUrl, { method: 'GET' }); - const encData = await encRes.arrayBuffer(); - const decryptedBlob = await decryptFile(encData, mimeType, encFile); - return URL.createObjectURL(decryptedBlob); - } - return httpUrl; - }; - type RoomTimelineProps = { room: Room; eventId?: string; @@ -311,9 +293,9 @@ const useTimelinePagination = ( range: offsetRange > 0 ? { - start: currentTimeline.range.start + offsetRange, - end: currentTimeline.range.end + offsetRange, - } + start: currentTimeline.range.start + offsetRange, + end: currentTimeline.range.end + offsetRange, + } : { ...currentTimeline.range }, })); }; @@ -332,7 +314,7 @@ const useTimelinePagination = ( if ( !paginationToken && getTimelinesEventsCount(lTimelines) !== - getTimelinesEventsCount(getLinkedTimelines(timelineToPaginate)) + getTimelinesEventsCount(getLinkedTimelines(timelineToPaginate)) ) { recalibratePagination(lTimelines, timelinesEventsCount, backwards); return; @@ -492,10 +474,10 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const [focusItem, setFocusItem] = useState< | { - index: number; - scrollTo: boolean; - highlight: boolean; - } + index: number; + scrollTo: boolean; + highlight: boolean; + } | undefined >(); const alive = useAlive(); @@ -729,7 +711,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const editableEvtId = editableEvt?.getId(); if (!editableEvtId) return; setEditId(editableEvtId); - evt.preventDefault() + evt.preventDefault(); } }, [mx, room, editor] @@ -1469,14 +1451,14 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const eventJSX = reactionOrEditEvent(mEvent) ? null : renderMatrixEvent( - mEvent.getType(), - typeof mEvent.getStateKey() === 'string', - mEventId, - mEvent, - item, - timelineSet, - collapsed - ); + mEvent.getType(), + typeof mEvent.getStateKey() === 'string', + mEventId, + mEvent, + item, + timelineSet, + collapsed + ); prevEvent = mEvent; isPrevRendered = !!eventJSX; @@ -1558,8 +1540,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli {!canPaginateBack && rangeAtStart && getItems().length > 0 && (
diff --git a/src/app/molecules/image-pack/ImagePack.jsx b/src/app/molecules/image-pack/ImagePack.jsx index 51ffd0d3..e4e6be2a 100644 --- a/src/app/molecules/image-pack/ImagePack.jsx +++ b/src/app/molecules/image-pack/ImagePack.jsx @@ -1,6 +1,4 @@ -import React, { - useState, useMemo, useReducer, useEffect, -} from 'react'; +import React, { useState, useMemo, useReducer, useEffect } from 'react'; import PropTypes from 'prop-types'; import './ImagePack.scss'; @@ -19,41 +17,41 @@ import ImagePackProfile from './ImagePackProfile'; import ImagePackItem from './ImagePackItem'; import ImagePackUpload from './ImagePackUpload'; import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; -const renameImagePackItem = (shortcode) => new Promise((resolve) => { - let isCompleted = false; +const renameImagePackItem = (shortcode) => + new Promise((resolve) => { + let isCompleted = false; - openReusableDialog( - Rename, - (requestClose) => ( -
-
{ - e.preventDefault(); - const sc = e.target.shortcode.value; - if (sc.trim() === '') return; - isCompleted = true; - resolve(sc.trim()); - requestClose(); - }} - > - -
- - -
- ), - () => { - if (!isCompleted) resolve(null); - }, - ); -}); + openReusableDialog( + + Rename + , + (requestClose) => ( +
+
{ + e.preventDefault(); + const sc = e.target.shortcode.value; + if (sc.trim() === '') return; + isCompleted = true; + resolve(sc.trim()); + requestClose(); + }} + > + +
+ + +
+ ), + () => { + if (!isCompleted) resolve(null); + } + ); + }); function getUsage(usage) { if (usage.includes('emoticon') && usage.includes('sticker')) return 'both'; @@ -79,7 +77,7 @@ function useRoomImagePack(roomId, stateKey) { const pack = useMemo(() => { const packEvent = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey); - return ImagePackBuilder.parsePack(packEvent.getId(), packEvent.getContent()) + return ImagePackBuilder.parsePack(packEvent.getId(), packEvent.getContent()); }, [room, stateKey]); const sendPackContent = (content) => { @@ -96,10 +94,13 @@ function useUserImagePack() { const mx = useMatrixClient(); const pack = useMemo(() => { const packEvent = mx.getAccountData('im.ponies.user_emotes'); - return ImagePackBuilder.parsePack(mx.getUserId(), packEvent?.getContent() ?? { - pack: { display_name: 'Personal' }, - images: {}, - }) + return ImagePackBuilder.parsePack( + mx.getUserId(), + packEvent?.getContent() ?? { + pack: { display_name: 'Personal' }, + images: {}, + } + ); }, [mx]); const sendPackContent = (content) => { @@ -119,10 +120,7 @@ function useImagePackHandles(pack, sendPackContent) { if (typeof key !== 'string') return undefined; let newKey = key?.replace(/\s/g, '_'); if (pack.getImages().get(newKey)) { - newKey = suffixRename( - newKey, - (suffixedKey) => pack.getImages().get(suffixedKey), - ); + newKey = suffixRename(newKey, (suffixedKey) => pack.getImages().get(suffixedKey)); } return newKey; }; @@ -163,7 +161,7 @@ function useImagePackHandles(pack, sendPackContent) { 'Delete', `Are you sure that you want to delete "${key}"?`, 'Delete', - 'danger', + 'danger' ); if (!isConfirmed) return; pack.removeImage(key); @@ -226,6 +224,7 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) { const room = mx.getRoom(roomId); const [viewMore, setViewMore] = useState(false); const [isGlobal, setIsGlobal] = useState(isGlobalPack(mx, roomId, stateKey)); + const useAuthentication = useMediaAuthentication(); const { pack, sendPackContent } = useRoomImagePack(roomId, stateKey); @@ -253,7 +252,7 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) { 'Delete Pack', `Are you sure that you want to delete "${pack.displayName}"?`, 'Delete', - 'danger', + 'danger' ); if (!isConfirmed) return; handlePackDelete(stateKey); @@ -264,7 +263,19 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) { return (
- { canChange && ( - - )} - { images.length === 0 ? null : ( + {canChange && } + {images.length === 0 ? null : (
Image @@ -285,7 +294,15 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) { {images.map(([shortcode, image]) => ( {pack.images.size > 2 && ( + )} + {handlePackDelete && ( + )} - { handlePackDelete && }
)}
@@ -332,6 +349,7 @@ ImagePack.propTypes = { function ImagePackUser() { const mx = useMatrixClient(); const [viewMore, setViewMore] = useState(false); + const useAuthentication = useMediaAuthentication(); const { pack, sendPackContent } = useUserImagePack(); @@ -350,7 +368,19 @@ function ImagePackUser() { return (
- { images.length === 0 ? null : ( + {images.length === 0 ? null : (
Image @@ -369,7 +399,15 @@ function ImagePackUser() { {images.map(([shortcode, image]) => ( )} - {(pack.images.size > 2) && ( + {pack.images.size > 2 && (
)} @@ -435,29 +469,33 @@ function ImagePackGlobal() {
Global packs
- { - roomIdToStateKeys.size > 0 - ? [...roomIdToStateKeys].map(([roomId, stateKeys]) => { - const room = mx.getRoom(roomId); + {roomIdToStateKeys.size > 0 ? ( + [...roomIdToStateKeys].map(([roomId, stateKeys]) => { + const room = mx.getRoom(roomId); + return stateKeys.map((stateKey) => { + const data = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey); + const pack = ImagePackBuilder.parsePack(data?.getId(), data?.getContent()); + if (!pack) return null; return ( - stateKeys.map((stateKey) => { - const data = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey); - const pack = ImagePackBuilder.parsePack(data?.getId(), data?.getContent()); - if (!pack) return null; - return ( -
- handleChange(roomId, stateKey)} isActive /> -
- {pack.displayName ?? 'Unknown'} - {room.name} -
-
- ); - }) +
+ handleChange(roomId, stateKey)} + isActive + /> +
+ {pack.displayName ?? 'Unknown'} + {room.name} +
+
); - }) - :
No global packs
- } + }); + }) + ) : ( +
+ No global packs +
+ )}
); diff --git a/src/app/organisms/invite-user/InviteUser.jsx b/src/app/organisms/invite-user/InviteUser.jsx index 284be72e..c5bade69 100644 --- a/src/app/organisms/invite-user/InviteUser.jsx +++ b/src/app/organisms/invite-user/InviteUser.jsx @@ -18,11 +18,13 @@ import UserIC from '../../../../public/res/ic/outlined/user.svg'; import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { getDMRoomFor } from '../../utils/matrix'; import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; function InviteUser({ isOpen, roomId, searchTerm, onRequestClose }) { const [isSearching, updateIsSearching] = useState(false); const [searchQuery, updateSearchQuery] = useState({}); const [users, updateUsers] = useState([]); + const useAuthentication = useMediaAuthentication(); const [procUsers, updateProcUsers] = useState(new Set()); // proc stands for processing. const [procUserError, updateUserProcError] = useState(new Map()); @@ -222,7 +224,15 @@ function InviteUser({ isOpen, roomId, searchTerm, onRequestClose }) { key={userId} avatarSrc={ typeof user.avatar_url === 'string' - ? mx.mxcUrlToHttp(user.avatar_url, 42, 42, 'crop') + ? mx.mxcUrlToHttp( + user.avatar_url, + 42, + 42, + 'crop', + undefined, + undefined, + useAuthentication + ) : null } name={name} diff --git a/src/app/organisms/profile-editor/ProfileEditor.jsx b/src/app/organisms/profile-editor/ProfileEditor.jsx index 95709355..9612002d 100644 --- a/src/app/organisms/profile-editor/ProfileEditor.jsx +++ b/src/app/organisms/profile-editor/ProfileEditor.jsx @@ -14,15 +14,19 @@ import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog'; import './ProfileEditor.scss'; import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; function ProfileEditor({ userId }) { const [isEditing, setIsEditing] = useState(false); const mx = useMatrixClient(); const user = mx.getUser(mx.getUserId()); + const useAuthentication = useMediaAuthentication(); const displayNameRef = useRef(null); const [avatarSrc, setAvatarSrc] = useState( - user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null + user.avatarUrl + ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop', undefined, undefined, useAuthentication) + : null ); const [username, setUsername] = useState(user.displayName); const [disabled, setDisabled] = useState(true); @@ -31,13 +35,25 @@ function ProfileEditor({ userId }) { let isMounted = true; mx.getProfileInfo(mx.getUserId()).then((info) => { if (!isMounted) return; - setAvatarSrc(info.avatar_url ? mx.mxcUrlToHttp(info.avatar_url, 80, 80, 'crop') : null); + setAvatarSrc( + info.avatar_url + ? mx.mxcUrlToHttp( + info.avatar_url, + 80, + 80, + 'crop', + undefined, + undefined, + useAuthentication + ) + : null + ); setUsername(info.displayname); }); return () => { isMounted = false; }; - }, [mx, userId]); + }, [mx, userId, useAuthentication]); const handleAvatarUpload = async (url) => { if (url === null) { @@ -54,7 +70,7 @@ function ProfileEditor({ userId }) { return; } mx.setAvatarUrl(url); - setAvatarSrc(mx.mxcUrlToHttp(url, 80, 80, 'crop')); + setAvatarSrc(mx.mxcUrlToHttp(url, 80, 80, 'crop', undefined, undefined, useAuthentication)); }; const saveDisplayName = () => { diff --git a/src/app/organisms/profile-viewer/ProfileViewer.jsx b/src/app/organisms/profile-viewer/ProfileViewer.jsx index b4ab7473..6ff5fe7c 100644 --- a/src/app/organisms/profile-viewer/ProfileViewer.jsx +++ b/src/app/organisms/profile-viewer/ProfileViewer.jsx @@ -36,6 +36,7 @@ import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog'; import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { getDMRoomFor } from '../../utils/matrix'; import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; function ModerationTools({ roomId, userId }) { const mx = useMatrixClient(); @@ -329,6 +330,7 @@ function useRerenderOnProfileChange(roomId, userId) { function ProfileViewer() { const [isOpen, roomId, userId, closeDialog, handleAfterClose] = useToggleDialog(); useRerenderOnProfileChange(roomId, userId); + const useAuthentication = useMediaAuthentication(); const mx = useMatrixClient(); const room = mx.getRoom(roomId); @@ -338,7 +340,9 @@ function ProfileViewer() { const username = roomMember ? getUsernameOfRoomMember(roomMember) : getUsername(mx, userId); const avatarMxc = roomMember?.getMxcAvatarUrl?.() || mx.getUser(userId)?.avatarUrl; const avatarUrl = - avatarMxc && avatarMxc !== 'null' ? mx.mxcUrlToHttp(avatarMxc, 80, 80, 'crop') : null; + avatarMxc && avatarMxc !== 'null' + ? mx.mxcUrlToHttp(avatarMxc, 80, 80, 'crop', undefined, undefined, useAuthentication) + : null; const powerLevel = roomMember?.powerLevel || 0; const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0; diff --git a/src/app/utils/matrix.ts b/src/app/utils/matrix.ts index 65a1080b..66975e7b 100644 --- a/src/app/utils/matrix.ts +++ b/src/app/utils/matrix.ts @@ -273,3 +273,20 @@ export const mxcUrlToHttp = ( allowRedirects, useAuthentication ); + +export const downloadMedia = async (src: string): Promise => { + // this request is authenticated by service worker + const res = await fetch(src, { method: 'GET' }); + const blob = await res.blob(); + return blob; +}; + +export const downloadEncryptedMedia = async ( + src: string, + decryptContent: (buf: ArrayBuffer) => Promise +): Promise => { + const encryptedContent = await downloadMedia(src); + const decryptedContent = await decryptContent(await encryptedContent.arrayBuffer()); + + return decryptedContent; +};