Fix authenticated media download (#1947)
authorAjay Bura <32841439+ajbura@users.noreply.github.com>
Wed, 11 Sep 2024 07:07:02 +0000 (17:07 +1000)
committerGitHub <noreply@github.com>
Wed, 11 Sep 2024 07:07:02 +0000 (17:07 +1000)
* remove dead function

* fix media download in room timeline

* authenticate remaining media endpoints

13 files changed:
src/app/components/image-viewer/ImageViewer.tsx
src/app/components/message/content/AudioContent.tsx
src/app/components/message/content/FileContent.tsx
src/app/components/message/content/ImageContent.tsx
src/app/components/message/content/ThumbnailContent.tsx
src/app/components/message/content/VideoContent.tsx
src/app/components/message/content/util.ts [deleted file]
src/app/features/room/RoomTimeline.tsx
src/app/molecules/image-pack/ImagePack.jsx
src/app/organisms/invite-user/InviteUser.jsx
src/app/organisms/profile-editor/ProfileEditor.jsx
src/app/organisms/profile-viewer/ProfileViewer.jsx
src/app/utils/matrix.ts

index 4fd06b7a7651a9ece63d0c73959bbf3b1f080524..4956a7b6dd03cbd610821cc21993e8526e1fa806 100644 (file)
@@ -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 (
index d6a82a3bc84e0a0e31b5b018cc6be0f38e20eee1..71551b12c5bedc9573df962a19d92e1059654454 100644 (file)
@@ -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<HTMLAudioElement | null>(null);
index 62b8c56dd339b4eb5dce8787931a0ed559080c92..c866dab57e73619005d7733288f2c5cdb4080345 100644 (file)
@@ -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])
   );
 
index d0a31c39c5f91aa17c6309300f63d51d86edccd4..0d1d6d3affd388c8ff4114e99f969767b46c0a04 100644 (file)
@@ -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 = () => {
index b667e57ebb41a1984af4dfe437fad412d118ae2c..0746137cb78b1e9d23c56ab34ff23a2de413f0c3 100644 (file)
@@ -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])
   );
 
index 1683075bffa0fde17736ebe0123e49814bdc87e0..f6ddbb5a5beb531cce4e34be5435ea17bd581062 100644 (file)
@@ -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 (file)
index 8614b8e..0000000
+++ /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<string> => {
-  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<Blob> => {
-  const res = await fetch(src, { method: 'GET' });
-  const blob = await res.blob();
-  return blob;
-};
index 34f54868d516194956f37c810139cf0a2513d5c8..a2738fcbf257c79411fba3e96823d7934f3444cc 100644 (file)
@@ -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<string> => {
-    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 && (
             <div
               style={{
-                padding: `${config.space.S700} ${config.space.S400} ${config.space.S600} ${messageLayout === 1 ? config.space.S400 : toRem(64)
-                  }`,
+                padding: `${config.space.S700} ${config.space.S400} ${config.space.S600} ${
+                  messageLayout === 1 ? config.space.S400 : toRem(64)
+                }`,
               }}
             >
               <RoomIntro room={room} />
index 51ffd0d35de82134fb6116fbc9bc06ce37686f71..e4e6be2aa58c77dc5706dbe36a79810b2e9cd488 100644 (file)
@@ -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';
-
-const renameImagePackItem = (shortcode) => new Promise((resolve) => {
-  let isCompleted = false;
-
-  openReusableDialog(
-    <Text variant="s1" weight="medium">Rename</Text>,
-    (requestClose) => (
-      <div style={{ padding: 'var(--sp-normal)' }}>
-        <form
-          onSubmit={(e) => {
-            e.preventDefault();
-            const sc = e.target.shortcode.value;
-            if (sc.trim() === '') return;
-            isCompleted = true;
-            resolve(sc.trim());
-            requestClose();
-          }}
-        >
-          <Input
-            value={shortcode}
-            name="shortcode"
-            label="Shortcode"
-            autoFocus
-            required
-          />
-          <div style={{ height: 'var(--sp-normal)' }} />
-          <Button variant="primary" type="submit">Rename</Button>
-        </form>
-      </div>
-    ),
-    () => {
-      if (!isCompleted) resolve(null);
-    },
-  );
-});
+import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
+
+const renameImagePackItem = (shortcode) =>
+  new Promise((resolve) => {
+    let isCompleted = false;
+
+    openReusableDialog(
+      <Text variant="s1" weight="medium">
+        Rename
+      </Text>,
+      (requestClose) => (
+        <div style={{ padding: 'var(--sp-normal)' }}>
+          <form
+            onSubmit={(e) => {
+              e.preventDefault();
+              const sc = e.target.shortcode.value;
+              if (sc.trim() === '') return;
+              isCompleted = true;
+              resolve(sc.trim());
+              requestClose();
+            }}
+          >
+            <Input value={shortcode} name="shortcode" label="Shortcode" autoFocus required />
+            <div style={{ height: 'var(--sp-normal)' }} />
+            <Button variant="primary" type="submit">
+              Rename
+            </Button>
+          </form>
+        </div>
+      ),
+      () => {
+        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 (
     <div className="image-pack">
       <ImagePackProfile
-        avatarUrl={pack.avatarUrl ? mx.mxcUrlToHttp(pack.avatarUrl, 42, 42, 'crop') : null}
+        avatarUrl={
+          pack.avatarUrl
+            ? mx.mxcUrlToHttp(
+                pack.avatarUrl,
+                42,
+                42,
+                'crop',
+                undefined,
+                undefined,
+                useAuthentication
+              )
+            : null
+        }
         displayName={pack.displayName ?? 'Unknown'}
         attribution={pack.attribution}
         usage={getUsage(pack.usage)}
@@ -272,10 +283,8 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
         onAvatarChange={canChange ? handleAvatarChange : null}
         onEditProfile={canChange ? handleEditProfile : null}
       />
-      { canChange && (
-        <ImagePackUpload onUpload={handleAddItem} />
-      )}
-      { images.length === 0 ? null : (
+      {canChange && <ImagePackUpload onUpload={handleAddItem} />}
+      {images.length === 0 ? null : (
         <div>
           <div className="image-pack__header">
             <Text variant="b3">Image</Text>
@@ -285,7 +294,15 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
           {images.map(([shortcode, image]) => (
             <ImagePackItem
               key={shortcode}
-              url={mx.mxcUrlToHttp(image.mxc)}
+              url={mx.mxcUrlToHttp(
+                image.mxc,
+                undefined,
+                undefined,
+                undefined,
+                undefined,
+                undefined,
+                useAuthentication
+              )}
               shortcode={shortcode}
               usage={getUsage(image.usage)}
               onUsageChange={canChange ? handleUsageItem : undefined}
@@ -299,14 +316,14 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
         <div className="image-pack__footer">
           {pack.images.size > 2 && (
             <Button onClick={() => setViewMore(!viewMore)}>
-              {
-                viewMore
-                  ? 'View less'
-                  : `View ${pack.images.size - 2} more`
-              }
+              {viewMore ? 'View less' : `View ${pack.images.size - 2} more`}
+            </Button>
+          )}
+          {handlePackDelete && (
+            <Button variant="danger" onClick={handleDeletePack}>
+              Delete Pack
             </Button>
           )}
-          { handlePackDelete && <Button variant="danger" onClick={handleDeletePack}>Delete Pack</Button>}
         </div>
       )}
       <div className="image-pack__global">
@@ -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 (
     <div className="image-pack">
       <ImagePackProfile
-        avatarUrl={pack.avatarUrl ? mx.mxcUrlToHttp(pack.avatarUrl, 42, 42, 'crop') : null}
+        avatarUrl={
+          pack.avatarUrl
+            ? mx.mxcUrlToHttp(
+                pack.avatarUrl,
+                42,
+                42,
+                'crop',
+                undefined,
+                undefined,
+                useAuthentication
+              )
+            : null
+        }
         displayName={pack.displayName ?? 'Personal'}
         attribution={pack.attribution}
         usage={getUsage(pack.usage)}
@@ -359,7 +389,7 @@ function ImagePackUser() {
         onEditProfile={handleEditProfile}
       />
       <ImagePackUpload onUpload={handleAddItem} />
-      { images.length === 0 ? null : (
+      {images.length === 0 ? null : (
         <div>
           <div className="image-pack__header">
             <Text variant="b3">Image</Text>
@@ -369,7 +399,15 @@ function ImagePackUser() {
           {images.map(([shortcode, image]) => (
             <ImagePackItem
               key={shortcode}
-              url={mx.mxcUrlToHttp(image.mxc)}
+              url={mx.mxcUrlToHttp(
+                image.mxc,
+                undefined,
+                undefined,
+                undefined,
+                undefined,
+                undefined,
+                useAuthentication
+              )}
               shortcode={shortcode}
               usage={getUsage(image.usage)}
               onUsageChange={handleUsageItem}
@@ -379,14 +417,10 @@ function ImagePackUser() {
           ))}
         </div>
       )}
-      {(pack.images.size > 2) && (
+      {pack.images.size > 2 && (
         <div className="image-pack__footer">
           <Button onClick={() => setViewMore(!viewMore)}>
-            {
-              viewMore
-                ? 'View less'
-                : `View ${pack.images.size - 2} more`
-            }
+            {viewMore ? 'View less' : `View ${pack.images.size - 2} more`}
           </Button>
         </div>
       )}
@@ -435,29 +469,33 @@ function ImagePackGlobal() {
     <div className="image-pack-global">
       <MenuHeader>Global packs</MenuHeader>
       <div>
-        {
-          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 (
-                    <div className="image-pack__global" key={pack.id}>
-                      <Checkbox variant="positive" onToggle={() => handleChange(roomId, stateKey)} isActive />
-                      <div>
-                        <Text variant="b2">{pack.displayName ?? 'Unknown'}</Text>
-                        <Text variant="b3">{room.name}</Text>
-                      </div>
-                    </div>
-                  );
-                })
+                <div className="image-pack__global" key={pack.id}>
+                  <Checkbox
+                    variant="positive"
+                    onToggle={() => handleChange(roomId, stateKey)}
+                    isActive
+                  />
+                  <div>
+                    <Text variant="b2">{pack.displayName ?? 'Unknown'}</Text>
+                    <Text variant="b3">{room.name}</Text>
+                  </div>
+                </div>
               );
-            })
-            : <div className="image-pack-global__empty"><Text>No global packs</Text></div>
-        }
+            });
+          })
+        ) : (
+          <div className="image-pack-global__empty">
+            <Text>No global packs</Text>
+          </div>
+        )}
       </div>
     </div>
   );
index 284be72e1df801963380292d3d96240dbb8033ba..c5bade690fd6c12dd44d433ecd6d97ed6a05f8a5 100644 (file)
@@ -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}
index 9570935574e91440ae6b5108a3630d3c719d49e8..9612002d6d9d62a5de9fd352b74392ea4418208d 100644 (file)
@@ -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 = () => {
index b4ab747304a5a393c1da5d1574659cda1a93a7ab..6ff5fe7c3940de59b14587a3d114c98616fac6b2 100644 (file)
@@ -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;
index 65a1080bcd589a18a7bc7bc4328d03c5ea10df64..66975e7b9676eaaeab7a4786d5fb263cff39f81f 100644 (file)
@@ -273,3 +273,20 @@ export const mxcUrlToHttp = (
     allowRedirects,
     useAuthentication
   );
+
+export const downloadMedia = async (src: string): Promise<Blob> => {
+  // 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<Blob>
+): Promise<Blob> => {
+  const encryptedContent = await downloadMedia(src);
+  const decryptedContent = await decryptContent(await encryptedContent.arrayBuffer());
+
+  return decryptedContent;
+};