mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 06:20:28 +03:00 
			
		
		
		
	Fix authenticated media download (#1947)
* remove dead function * fix media download in room timeline * authenticate remaining media endpoints
This commit is contained in:
		
							parent
							
								
									f2c31d29a2
								
							
						
					
					
						commit
						03cc25eec0
					
				
					 13 changed files with 284 additions and 207 deletions
				
			
		| 
						 | 
				
			
			@ -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 (
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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])
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 = () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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])
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 = () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -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} />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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(
 | 
			
		||||
    <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);
 | 
			
		||||
    },
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
    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>
 | 
			
		||||
  );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 = () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue