1
0
Fork 0
forked from mirror/cinny

Merge remote-tracking branch 'upstream/dev' into custom

This commit is contained in:
tezlm 2024-09-11 12:23:59 -07:00
commit f7d0c10cb0
Signed by: tezlm
GPG key ID: 649733FCD94AFBBA
19 changed files with 296 additions and 212 deletions

View file

@ -10,7 +10,7 @@
cinny = pkgs.buildNpmPackage { cinny = pkgs.buildNpmPackage {
name = "cinny"; name = "cinny";
src = ./.; src = ./.;
npmDepsHash = "sha256-tKCQbvi8jW5lxthDiqARRxisWdb4inxNqLM0QhpV220="; npmDepsHash = "sha256-43nCceq1hR89m/xvC+89qxHGdtOUKjZU/MAz4pYu8K0=";
nativeBuildInputs = with pkgs; [ python3 pkg-config ]; nativeBuildInputs = with pkgs; [ python3 pkg-config ];
buildInputs = with pkgs; [ pixman cairo pango ]; buildInputs = with pkgs; [ pixman cairo pango ];
installPhase = '' installPhase = ''

12
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "cinny", "name": "cinny",
"version": "4.1.0", "version": "4.2.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cinny", "name": "cinny",
"version": "4.1.0", "version": "4.2.0",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "1.1.6", "@atlaskit/pragmatic-drag-and-drop": "1.1.6",
@ -45,7 +45,7 @@
"jotai": "2.6.0", "jotai": "2.6.0",
"linkify-react": "4.1.3", "linkify-react": "4.1.3",
"linkifyjs": "4.1.3", "linkifyjs": "4.1.3",
"matrix-js-sdk": "34.4.0", "matrix-js-sdk": "34.5.0",
"millify": "6.1.0", "millify": "6.1.0",
"pdfjs-dist": "4.2.67", "pdfjs-dist": "4.2.67",
"prismjs": "1.29.0", "prismjs": "1.29.0",
@ -9117,9 +9117,9 @@
"integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==" "integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA=="
}, },
"node_modules/matrix-js-sdk": { "node_modules/matrix-js-sdk": {
"version": "34.4.0", "version": "34.5.0",
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-34.4.0.tgz", "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-34.5.0.tgz",
"integrity": "sha512-bI5xJZS3/qhjPQqQL5HhOQ1iBvnHxiqhS2zgzk9SarEuXiH08wbVl9gAAuDqOYE3miNGs4WQQJ19MoaUEOnNwg==", "integrity": "sha512-pbp+IxAkSwGmefrlUGCrtrs3UWyqN2iWh4lKnJW+jFIlsksXq7A8vL4cS1z8LXmpcHQAg3mKNuj8n8uhm51t1A==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",

View file

@ -1,6 +1,6 @@
{ {
"name": "cinny", "name": "cinny",
"version": "4.1.0", "version": "4.2.0",
"description": "Yet another matrix client", "description": "Yet another matrix client",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
@ -57,7 +57,7 @@
"jotai": "2.6.0", "jotai": "2.6.0",
"linkify-react": "4.1.3", "linkify-react": "4.1.3",
"linkifyjs": "4.1.3", "linkifyjs": "4.1.3",
"matrix-js-sdk": "34.4.0", "matrix-js-sdk": "34.5.0",
"millify": "6.1.0", "millify": "6.1.0",
"pdfjs-dist": "4.2.67", "pdfjs-dist": "4.2.67",
"prismjs": "1.29.0", "prismjs": "1.29.0",

View file

@ -6,6 +6,7 @@ import { Box, Chip, Header, Icon, IconButton, Icons, Text, as } from 'folds';
import * as css from './ImageViewer.css'; import * as css from './ImageViewer.css';
import { useZoom } from '../../hooks/useZoom'; import { useZoom } from '../../hooks/useZoom';
import { usePan } from '../../hooks/usePan'; import { usePan } from '../../hooks/usePan';
import { downloadMedia } from '../../utils/matrix';
export type ImageViewerProps = { export type ImageViewerProps = {
alt: string; alt: string;
@ -18,8 +19,9 @@ export const ImageViewer = as<'div', ImageViewerProps>(
const { zoom, zoomIn, zoomOut, setZoom } = useZoom(0.2); const { zoom, zoomIn, zoomOut, setZoom } = useZoom(0.2);
const { pan, cursor, onMouseDown } = usePan(zoom !== 1); const { pan, cursor, onMouseDown } = usePan(zoom !== 1);
const handleDownload = () => { const handleDownload = async () => {
FileSaver.saveAs(src, alt); const fileContent = await downloadMedia(src);
FileSaver.saveAs(fileContent, alt);
}; };
return ( return (

View file

@ -5,7 +5,6 @@ import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
import { Range } from 'react-range'; import { Range } from 'react-range';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { getFileSrcUrl } from './util';
import { IAudioInfo } from '../../../../types/matrix/common'; import { IAudioInfo } from '../../../../types/matrix/common';
import { import {
PlayTimeCallback, PlayTimeCallback,
@ -17,7 +16,12 @@ import {
} from '../../../hooks/media'; } from '../../../hooks/media';
import { useThrottle } from '../../../hooks/useThrottle'; import { useThrottle } from '../../../hooks/useThrottle';
import { secondsToMinutesAndSeconds } from '../../../utils/common'; import { secondsToMinutesAndSeconds } from '../../../utils/common';
import { mxcUrlToHttp } from '../../../utils/matrix'; import {
decryptFile,
downloadEncryptedMedia,
downloadMedia,
mxcUrlToHttp,
} from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
const PLAY_TIME_THROTTLE_OPS = { const PLAY_TIME_THROTTLE_OPS = {
@ -49,10 +53,13 @@ export function AudioContent({
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
const [srcState, loadSrc] = useAsyncCallback( const [srcState, loadSrc] = useAsyncCallback(
useCallback( useCallback(async () => {
() => getFileSrcUrl(mxcUrlToHttp(mx, url, useAuthentication) ?? '', mimeType, encInfo), const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
[mx, url, useAuthentication, mimeType, encInfo] 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); const audioRef = useRef<HTMLAudioElement | null>(null);

View file

@ -20,7 +20,6 @@ import FocusTrap from 'focus-trap-react';
import { IFileInfo } from '../../../../types/matrix/common'; import { IFileInfo } from '../../../../types/matrix/common';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { getFileSrcUrl, getSrcFile } from './util';
import { bytesToSize } from '../../../utils/common'; import { bytesToSize } from '../../../utils/common';
import { import {
READABLE_EXT_TO_MIME_TYPE, READABLE_EXT_TO_MIME_TYPE,
@ -30,7 +29,12 @@ import {
} from '../../../utils/mimeTypes'; } from '../../../utils/mimeTypes';
import * as css from './style.css'; import * as css from './style.css';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { mxcUrlToHttp } from '../../../utils/matrix'; import {
decryptFile,
downloadEncryptedMedia,
downloadMedia,
mxcUrlToHttp,
} from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
const renderErrorButton = (retry: () => void, text: string) => ( const renderErrorButton = (retry: () => void, text: string) => (
@ -80,19 +84,17 @@ export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: Rea
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
const [textViewer, setTextViewer] = useState(false); const [textViewer, setTextViewer] = useState(false);
const loadSrc = useCallback(
() => getFileSrcUrl(mxcUrlToHttp(mx, url, useAuthentication) ?? '', mimeType, encInfo),
[mx, url, useAuthentication, mimeType, encInfo]
);
const [textState, loadText] = useAsyncCallback( const [textState, loadText] = useAsyncCallback(
useCallback(async () => { useCallback(async () => {
const src = await loadSrc(); const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
const blob = await getSrcFile(src); const fileContent = encInfo
const text = blob.text(); ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
: await downloadMedia(mediaUrl);
const text = fileContent.text();
setTextViewer(true); setTextViewer(true);
return text; return text;
}, [loadSrc]) }, [mx, useAuthentication, mimeType, encInfo, url])
); );
return ( return (
@ -174,9 +176,12 @@ export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: Read
const [pdfState, loadPdf] = useAsyncCallback( const [pdfState, loadPdf] = useAsyncCallback(
useCallback(async () => { 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); setPdfViewer(true);
return httpUrl; return URL.createObjectURL(fileContent);
}, [mx, url, useAuthentication, mimeType, encInfo]) }, [mx, url, useAuthentication, mimeType, encInfo])
); );
@ -248,9 +253,14 @@ export function DownloadFile({ body, mimeType, url, info, encInfo }: DownloadFil
const [downloadState, download] = useAsyncCallback( const [downloadState, download] = useAsyncCallback(
useCallback(async () => { useCallback(async () => {
const httpUrl = await getFileSrcUrl(mxcUrlToHttp(mx, url, useAuthentication) ?? '', mimeType, encInfo); const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
FileSaver.saveAs(httpUrl, body); const fileContent = encInfo
return httpUrl; ? 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]) }, [mx, url, useAuthentication, mimeType, encInfo, body])
); );

View file

@ -22,12 +22,11 @@ import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
import { IImageInfo, MATRIX_BLUR_HASH_PROPERTY_NAME } from '../../../../types/matrix/common'; import { IImageInfo, MATRIX_BLUR_HASH_PROPERTY_NAME } from '../../../../types/matrix/common';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { getFileSrcUrl } from './util';
import * as css from './style.css'; import * as css from './style.css';
import { bytesToSize } from '../../../utils/common'; import { bytesToSize } from '../../../utils/common';
import { FALLBACK_MIMETYPE } from '../../../utils/mimeTypes'; import { FALLBACK_MIMETYPE } from '../../../utils/mimeTypes';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { mxcUrlToHttp } from '../../../utils/matrix'; import { decryptFile, downloadEncryptedMedia, mxcUrlToHttp } from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
type RenderViewerProps = { type RenderViewerProps = {
@ -79,10 +78,16 @@ export const ImageContent = as<'div', ImageContentProps>(
const [viewer, setViewer] = useState(false); const [viewer, setViewer] = useState(false);
const [srcState, loadSrc] = useAsyncCallback( const [srcState, loadSrc] = useAsyncCallback(
useCallback( useCallback(async () => {
() => getFileSrcUrl(mxcUrlToHttp(mx, url, useAuthentication) ?? '', mimeType || FALLBACK_MIMETYPE, encInfo), const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
[mx, url, useAuthentication, mimeType, encInfo] 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 = () => { const handleLoad = () => {

View file

@ -2,9 +2,9 @@ import { ReactNode, useCallback, useEffect } from 'react';
import { IThumbnailContent } from '../../../../types/matrix/common'; import { IThumbnailContent } from '../../../../types/matrix/common';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { getFileSrcUrl } from './util'; import { decryptFile, downloadEncryptedMedia, mxcUrlToHttp } from '../../../utils/matrix';
import { mxcUrlToHttp } from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { FALLBACK_MIMETYPE } from '../../../utils/mimeTypes';
export type ThumbnailContentProps = { export type ThumbnailContentProps = {
info: IThumbnailContent; info: IThumbnailContent;
@ -15,17 +15,23 @@ export function ThumbnailContent({ info, renderImage }: ThumbnailContentProps) {
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
const [thumbSrcState, loadThumbSrc] = useAsyncCallback( const [thumbSrcState, loadThumbSrc] = useAsyncCallback(
useCallback(() => { useCallback(async () => {
const thumbInfo = info.thumbnail_info; const thumbInfo = info.thumbnail_info;
const thumbMxcUrl = info.thumbnail_file?.url ?? info.thumbnail_url; const thumbMxcUrl = info.thumbnail_file?.url ?? info.thumbnail_url;
const encInfo = info.thumbnail_file;
if (typeof thumbMxcUrl !== 'string' || typeof thumbInfo?.mimetype !== 'string') { if (typeof thumbMxcUrl !== 'string' || typeof thumbInfo?.mimetype !== 'string') {
throw new Error('Failed to load thumbnail'); throw new Error('Failed to load thumbnail');
} }
return getFileSrcUrl(
mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication) ?? '', const mediaUrl = mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication) ?? thumbMxcUrl;
thumbInfo.mimetype, if (encInfo) {
info.thumbnail_file const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
); decryptFile(encBuf, thumbInfo.mimetype ?? FALLBACK_MIMETYPE, encInfo)
);
return URL.createObjectURL(fileContent);
}
return mediaUrl;
}, [mx, info, useAuthentication]) }, [mx, info, useAuthentication])
); );

View file

@ -22,10 +22,14 @@ import {
import * as css from './style.css'; import * as css from './style.css';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { getFileSrcUrl } from './util';
import { bytesToSize } from '../../../../util/common'; import { bytesToSize } from '../../../../util/common';
import { millisecondsToMinutesAndSeconds } from '../../../utils/common'; import { millisecondsToMinutesAndSeconds } from '../../../utils/common';
import { mxcUrlToHttp } from '../../../utils/matrix'; import {
decryptFile,
downloadEncryptedMedia,
downloadMedia,
mxcUrlToHttp,
} from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
type RenderVideoProps = { type RenderVideoProps = {
@ -70,10 +74,15 @@ export const VideoContent = as<'div', VideoContentProps>(
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [srcState, loadSrc] = useAsyncCallback( const [srcState, loadSrc] = useAsyncCallback(
useCallback( useCallback(async () => {
() => getFileSrcUrl(mxcUrlToHttp(mx, url, useAuthentication) ?? '', mimeType, encInfo), const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
[mx, url, useAuthentication, mimeType, encInfo] 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 = () => { const handleLoad = () => {

View file

@ -1,23 +0,0 @@
import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
import { decryptFile } from '../../../utils/matrix';
export const getFileSrcUrl = async (
httpUrl: string,
mimeType: string,
encInfo?: EncryptedAttachmentInfo
): 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);
}
return httpUrl;
};
export const getSrcFile = async (src: string): Promise<Blob> => {
const res = await fetch(src, { method: 'GET' });
const blob = await res.blob();
return blob;
};

View file

@ -17,7 +17,6 @@ import {
EventTimelineSet, EventTimelineSet,
EventTimelineSetHandlerMap, EventTimelineSetHandlerMap,
IContent, IContent,
IEncryptedFile,
MatrixClient, MatrixClient,
MatrixEvent, MatrixEvent,
Room, Room,
@ -48,12 +47,7 @@ import {
import { isKeyHotkey } from 'is-hotkey'; import { isKeyHotkey } from 'is-hotkey';
import { Opts as LinkifyOpts } from 'linkifyjs'; import { Opts as LinkifyOpts } from 'linkifyjs';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import { eventWithShortcode, factoryEventSentBy, getMxIdLocalPart } from '../../utils/matrix';
decryptFile,
eventWithShortcode,
factoryEventSentBy,
getMxIdLocalPart,
} from '../../utils/matrix';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useVirtualPaginator, ItemRange } from '../../hooks/useVirtualPaginator'; import { useVirtualPaginator, ItemRange } from '../../hooks/useVirtualPaginator';
import { useAlive } from '../../hooks/useAlive'; import { useAlive } from '../../hooks/useAlive';
@ -220,18 +214,6 @@ export const getEventIdAbsoluteIndex = (
return baseIndex + eventIndex; 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 = { type RoomTimelineProps = {
room: Room; room: Room;
eventId?: string; eventId?: string;
@ -311,9 +293,9 @@ const useTimelinePagination = (
range: range:
offsetRange > 0 offsetRange > 0
? { ? {
start: currentTimeline.range.start + offsetRange, start: currentTimeline.range.start + offsetRange,
end: currentTimeline.range.end + offsetRange, end: currentTimeline.range.end + offsetRange,
} }
: { ...currentTimeline.range }, : { ...currentTimeline.range },
})); }));
}; };
@ -332,7 +314,7 @@ const useTimelinePagination = (
if ( if (
!paginationToken && !paginationToken &&
getTimelinesEventsCount(lTimelines) !== getTimelinesEventsCount(lTimelines) !==
getTimelinesEventsCount(getLinkedTimelines(timelineToPaginate)) getTimelinesEventsCount(getLinkedTimelines(timelineToPaginate))
) { ) {
recalibratePagination(lTimelines, timelinesEventsCount, backwards); recalibratePagination(lTimelines, timelinesEventsCount, backwards);
return; return;
@ -492,10 +474,10 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const [focusItem, setFocusItem] = useState< const [focusItem, setFocusItem] = useState<
| { | {
index: number; index: number;
scrollTo: boolean; scrollTo: boolean;
highlight: boolean; highlight: boolean;
} }
| undefined | undefined
>(); >();
const alive = useAlive(); const alive = useAlive();
@ -729,7 +711,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const editableEvtId = editableEvt?.getId(); const editableEvtId = editableEvt?.getId();
if (!editableEvtId) return; if (!editableEvtId) return;
setEditId(editableEvtId); setEditId(editableEvtId);
evt.preventDefault() evt.preventDefault();
} }
}, },
[mx, room, editor] [mx, room, editor]
@ -1469,14 +1451,14 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const eventJSX = reactionOrEditEvent(mEvent) const eventJSX = reactionOrEditEvent(mEvent)
? null ? null
: renderMatrixEvent( : renderMatrixEvent(
mEvent.getType(), mEvent.getType(),
typeof mEvent.getStateKey() === 'string', typeof mEvent.getStateKey() === 'string',
mEventId, mEventId,
mEvent, mEvent,
item, item,
timelineSet, timelineSet,
collapsed collapsed
); );
prevEvent = mEvent; prevEvent = mEvent;
isPrevRendered = !!eventJSX; isPrevRendered = !!eventJSX;
@ -1558,8 +1540,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
{!canPaginateBack && rangeAtStart && getItems().length > 0 && ( {!canPaginateBack && rangeAtStart && getItems().length > 0 && (
<div <div
style={{ 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} /> <RoomIntro room={room} />

View file

@ -1,6 +1,4 @@
import React, { import React, { useState, useMemo, useReducer, useEffect } from 'react';
useState, useMemo, useReducer, useEffect,
} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import './ImagePack.scss'; import './ImagePack.scss';
@ -19,41 +17,41 @@ import ImagePackProfile from './ImagePackProfile';
import ImagePackItem from './ImagePackItem'; import ImagePackItem from './ImagePackItem';
import ImagePackUpload from './ImagePackUpload'; import ImagePackUpload from './ImagePackUpload';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
const renameImagePackItem = (shortcode) => new Promise((resolve) => { const renameImagePackItem = (shortcode) =>
let isCompleted = false; new Promise((resolve) => {
let isCompleted = false;
openReusableDialog( openReusableDialog(
<Text variant="s1" weight="medium">Rename</Text>, <Text variant="s1" weight="medium">
(requestClose) => ( Rename
<div style={{ padding: 'var(--sp-normal)' }}> </Text>,
<form (requestClose) => (
onSubmit={(e) => { <div style={{ padding: 'var(--sp-normal)' }}>
e.preventDefault(); <form
const sc = e.target.shortcode.value; onSubmit={(e) => {
if (sc.trim() === '') return; e.preventDefault();
isCompleted = true; const sc = e.target.shortcode.value;
resolve(sc.trim()); if (sc.trim() === '') return;
requestClose(); isCompleted = true;
}} resolve(sc.trim());
> requestClose();
<Input }}
value={shortcode} >
name="shortcode" <Input value={shortcode} name="shortcode" label="Shortcode" autoFocus required />
label="Shortcode" <div style={{ height: 'var(--sp-normal)' }} />
autoFocus <Button variant="primary" type="submit">
required Rename
/> </Button>
<div style={{ height: 'var(--sp-normal)' }} /> </form>
<Button variant="primary" type="submit">Rename</Button> </div>
</form> ),
</div> () => {
), if (!isCompleted) resolve(null);
() => { }
if (!isCompleted) resolve(null); );
}, });
);
});
function getUsage(usage) { function getUsage(usage) {
if (usage.includes('emoticon') && usage.includes('sticker')) return 'both'; if (usage.includes('emoticon') && usage.includes('sticker')) return 'both';
@ -79,7 +77,7 @@ function useRoomImagePack(roomId, stateKey) {
const pack = useMemo(() => { const pack = useMemo(() => {
const packEvent = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey); 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]); }, [room, stateKey]);
const sendPackContent = (content) => { const sendPackContent = (content) => {
@ -96,10 +94,13 @@ function useUserImagePack() {
const mx = useMatrixClient(); const mx = useMatrixClient();
const pack = useMemo(() => { const pack = useMemo(() => {
const packEvent = mx.getAccountData('im.ponies.user_emotes'); const packEvent = mx.getAccountData('im.ponies.user_emotes');
return ImagePackBuilder.parsePack(mx.getUserId(), packEvent?.getContent() ?? { return ImagePackBuilder.parsePack(
pack: { display_name: 'Personal' }, mx.getUserId(),
images: {}, packEvent?.getContent() ?? {
}) pack: { display_name: 'Personal' },
images: {},
}
);
}, [mx]); }, [mx]);
const sendPackContent = (content) => { const sendPackContent = (content) => {
@ -119,10 +120,7 @@ function useImagePackHandles(pack, sendPackContent) {
if (typeof key !== 'string') return undefined; if (typeof key !== 'string') return undefined;
let newKey = key?.replace(/\s/g, '_'); let newKey = key?.replace(/\s/g, '_');
if (pack.getImages().get(newKey)) { if (pack.getImages().get(newKey)) {
newKey = suffixRename( newKey = suffixRename(newKey, (suffixedKey) => pack.getImages().get(suffixedKey));
newKey,
(suffixedKey) => pack.getImages().get(suffixedKey),
);
} }
return newKey; return newKey;
}; };
@ -163,7 +161,7 @@ function useImagePackHandles(pack, sendPackContent) {
'Delete', 'Delete',
`Are you sure that you want to delete "${key}"?`, `Are you sure that you want to delete "${key}"?`,
'Delete', 'Delete',
'danger', 'danger'
); );
if (!isConfirmed) return; if (!isConfirmed) return;
pack.removeImage(key); pack.removeImage(key);
@ -226,6 +224,7 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
const [viewMore, setViewMore] = useState(false); const [viewMore, setViewMore] = useState(false);
const [isGlobal, setIsGlobal] = useState(isGlobalPack(mx, roomId, stateKey)); const [isGlobal, setIsGlobal] = useState(isGlobalPack(mx, roomId, stateKey));
const useAuthentication = useMediaAuthentication();
const { pack, sendPackContent } = useRoomImagePack(roomId, stateKey); const { pack, sendPackContent } = useRoomImagePack(roomId, stateKey);
@ -253,7 +252,7 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
'Delete Pack', 'Delete Pack',
`Are you sure that you want to delete "${pack.displayName}"?`, `Are you sure that you want to delete "${pack.displayName}"?`,
'Delete', 'Delete',
'danger', 'danger'
); );
if (!isConfirmed) return; if (!isConfirmed) return;
handlePackDelete(stateKey); handlePackDelete(stateKey);
@ -264,7 +263,19 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
return ( return (
<div className="image-pack"> <div className="image-pack">
<ImagePackProfile <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'} displayName={pack.displayName ?? 'Unknown'}
attribution={pack.attribution} attribution={pack.attribution}
usage={getUsage(pack.usage)} usage={getUsage(pack.usage)}
@ -272,10 +283,8 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
onAvatarChange={canChange ? handleAvatarChange : null} onAvatarChange={canChange ? handleAvatarChange : null}
onEditProfile={canChange ? handleEditProfile : null} onEditProfile={canChange ? handleEditProfile : null}
/> />
{ canChange && ( {canChange && <ImagePackUpload onUpload={handleAddItem} />}
<ImagePackUpload onUpload={handleAddItem} /> {images.length === 0 ? null : (
)}
{ images.length === 0 ? null : (
<div> <div>
<div className="image-pack__header"> <div className="image-pack__header">
<Text variant="b3">Image</Text> <Text variant="b3">Image</Text>
@ -285,7 +294,15 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
{images.map(([shortcode, image]) => ( {images.map(([shortcode, image]) => (
<ImagePackItem <ImagePackItem
key={shortcode} key={shortcode}
url={mx.mxcUrlToHttp(image.mxc)} url={mx.mxcUrlToHttp(
image.mxc,
undefined,
undefined,
undefined,
undefined,
undefined,
useAuthentication
)}
shortcode={shortcode} shortcode={shortcode}
usage={getUsage(image.usage)} usage={getUsage(image.usage)}
onUsageChange={canChange ? handleUsageItem : undefined} onUsageChange={canChange ? handleUsageItem : undefined}
@ -299,14 +316,14 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
<div className="image-pack__footer"> <div className="image-pack__footer">
{pack.images.size > 2 && ( {pack.images.size > 2 && (
<Button onClick={() => setViewMore(!viewMore)}> <Button onClick={() => setViewMore(!viewMore)}>
{ {viewMore ? 'View less' : `View ${pack.images.size - 2} more`}
viewMore </Button>
? 'View less' )}
: `View ${pack.images.size - 2} more` {handlePackDelete && (
} <Button variant="danger" onClick={handleDeletePack}>
Delete Pack
</Button> </Button>
)} )}
{ handlePackDelete && <Button variant="danger" onClick={handleDeletePack}>Delete Pack</Button>}
</div> </div>
)} )}
<div className="image-pack__global"> <div className="image-pack__global">
@ -332,6 +349,7 @@ ImagePack.propTypes = {
function ImagePackUser() { function ImagePackUser() {
const mx = useMatrixClient(); const mx = useMatrixClient();
const [viewMore, setViewMore] = useState(false); const [viewMore, setViewMore] = useState(false);
const useAuthentication = useMediaAuthentication();
const { pack, sendPackContent } = useUserImagePack(); const { pack, sendPackContent } = useUserImagePack();
@ -350,7 +368,19 @@ function ImagePackUser() {
return ( return (
<div className="image-pack"> <div className="image-pack">
<ImagePackProfile <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'} displayName={pack.displayName ?? 'Personal'}
attribution={pack.attribution} attribution={pack.attribution}
usage={getUsage(pack.usage)} usage={getUsage(pack.usage)}
@ -359,7 +389,7 @@ function ImagePackUser() {
onEditProfile={handleEditProfile} onEditProfile={handleEditProfile}
/> />
<ImagePackUpload onUpload={handleAddItem} /> <ImagePackUpload onUpload={handleAddItem} />
{ images.length === 0 ? null : ( {images.length === 0 ? null : (
<div> <div>
<div className="image-pack__header"> <div className="image-pack__header">
<Text variant="b3">Image</Text> <Text variant="b3">Image</Text>
@ -369,7 +399,15 @@ function ImagePackUser() {
{images.map(([shortcode, image]) => ( {images.map(([shortcode, image]) => (
<ImagePackItem <ImagePackItem
key={shortcode} key={shortcode}
url={mx.mxcUrlToHttp(image.mxc)} url={mx.mxcUrlToHttp(
image.mxc,
undefined,
undefined,
undefined,
undefined,
undefined,
useAuthentication
)}
shortcode={shortcode} shortcode={shortcode}
usage={getUsage(image.usage)} usage={getUsage(image.usage)}
onUsageChange={handleUsageItem} onUsageChange={handleUsageItem}
@ -379,14 +417,10 @@ function ImagePackUser() {
))} ))}
</div> </div>
)} )}
{(pack.images.size > 2) && ( {pack.images.size > 2 && (
<div className="image-pack__footer"> <div className="image-pack__footer">
<Button onClick={() => setViewMore(!viewMore)}> <Button onClick={() => setViewMore(!viewMore)}>
{ {viewMore ? 'View less' : `View ${pack.images.size - 2} more`}
viewMore
? 'View less'
: `View ${pack.images.size - 2} more`
}
</Button> </Button>
</div> </div>
)} )}
@ -435,29 +469,33 @@ function ImagePackGlobal() {
<div className="image-pack-global"> <div className="image-pack-global">
<MenuHeader>Global packs</MenuHeader> <MenuHeader>Global packs</MenuHeader>
<div> <div>
{ {roomIdToStateKeys.size > 0 ? (
roomIdToStateKeys.size > 0 [...roomIdToStateKeys].map(([roomId, stateKeys]) => {
? [...roomIdToStateKeys].map(([roomId, stateKeys]) => { const room = mx.getRoom(roomId);
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 ( return (
stateKeys.map((stateKey) => { <div className="image-pack__global" key={pack.id}>
const data = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey); <Checkbox
const pack = ImagePackBuilder.parsePack(data?.getId(), data?.getContent()); variant="positive"
if (!pack) return null; onToggle={() => handleChange(roomId, stateKey)}
return ( isActive
<div className="image-pack__global" key={pack.id}> />
<Checkbox variant="positive" onToggle={() => handleChange(roomId, stateKey)} isActive /> <div>
<div> <Text variant="b2">{pack.displayName ?? 'Unknown'}</Text>
<Text variant="b2">{pack.displayName ?? 'Unknown'}</Text> <Text variant="b3">{room.name}</Text>
<Text variant="b3">{room.name}</Text> </div>
</div> </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>
</div> </div>
); );

View file

@ -18,11 +18,13 @@ import UserIC from '../../../../public/res/ic/outlined/user.svg';
import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { getDMRoomFor } from '../../utils/matrix'; import { getDMRoomFor } from '../../utils/matrix';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
function InviteUser({ isOpen, roomId, searchTerm, onRequestClose }) { function InviteUser({ isOpen, roomId, searchTerm, onRequestClose }) {
const [isSearching, updateIsSearching] = useState(false); const [isSearching, updateIsSearching] = useState(false);
const [searchQuery, updateSearchQuery] = useState({}); const [searchQuery, updateSearchQuery] = useState({});
const [users, updateUsers] = useState([]); const [users, updateUsers] = useState([]);
const useAuthentication = useMediaAuthentication();
const [procUsers, updateProcUsers] = useState(new Set()); // proc stands for processing. const [procUsers, updateProcUsers] = useState(new Set()); // proc stands for processing.
const [procUserError, updateUserProcError] = useState(new Map()); const [procUserError, updateUserProcError] = useState(new Map());
@ -222,7 +224,15 @@ function InviteUser({ isOpen, roomId, searchTerm, onRequestClose }) {
key={userId} key={userId}
avatarSrc={ avatarSrc={
typeof user.avatar_url === 'string' 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 : null
} }
name={name} name={name}

View file

@ -14,15 +14,19 @@ import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import './ProfileEditor.scss'; import './ProfileEditor.scss';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
function ProfileEditor({ userId }) { function ProfileEditor({ userId }) {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const mx = useMatrixClient(); const mx = useMatrixClient();
const user = mx.getUser(mx.getUserId()); const user = mx.getUser(mx.getUserId());
const useAuthentication = useMediaAuthentication();
const displayNameRef = useRef(null); const displayNameRef = useRef(null);
const [avatarSrc, setAvatarSrc] = useState( 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 [username, setUsername] = useState(user.displayName);
const [disabled, setDisabled] = useState(true); const [disabled, setDisabled] = useState(true);
@ -31,13 +35,25 @@ function ProfileEditor({ userId }) {
let isMounted = true; let isMounted = true;
mx.getProfileInfo(mx.getUserId()).then((info) => { mx.getProfileInfo(mx.getUserId()).then((info) => {
if (!isMounted) return; 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); setUsername(info.displayname);
}); });
return () => { return () => {
isMounted = false; isMounted = false;
}; };
}, [mx, userId]); }, [mx, userId, useAuthentication]);
const handleAvatarUpload = async (url) => { const handleAvatarUpload = async (url) => {
if (url === null) { if (url === null) {
@ -54,7 +70,7 @@ function ProfileEditor({ userId }) {
return; return;
} }
mx.setAvatarUrl(url); mx.setAvatarUrl(url);
setAvatarSrc(mx.mxcUrlToHttp(url, 80, 80, 'crop')); setAvatarSrc(mx.mxcUrlToHttp(url, 80, 80, 'crop', undefined, undefined, useAuthentication));
}; };
const saveDisplayName = () => { const saveDisplayName = () => {

View file

@ -36,6 +36,7 @@ import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { getDMRoomFor } from '../../utils/matrix'; import { getDMRoomFor } from '../../utils/matrix';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
function ModerationTools({ roomId, userId }) { function ModerationTools({ roomId, userId }) {
const mx = useMatrixClient(); const mx = useMatrixClient();
@ -329,6 +330,7 @@ function useRerenderOnProfileChange(roomId, userId) {
function ProfileViewer() { function ProfileViewer() {
const [isOpen, roomId, userId, closeDialog, handleAfterClose] = useToggleDialog(); const [isOpen, roomId, userId, closeDialog, handleAfterClose] = useToggleDialog();
useRerenderOnProfileChange(roomId, userId); useRerenderOnProfileChange(roomId, userId);
const useAuthentication = useMediaAuthentication();
const mx = useMatrixClient(); const mx = useMatrixClient();
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
@ -338,7 +340,9 @@ function ProfileViewer() {
const username = roomMember ? getUsernameOfRoomMember(roomMember) : getUsername(mx, userId); const username = roomMember ? getUsernameOfRoomMember(roomMember) : getUsername(mx, userId);
const avatarMxc = roomMember?.getMxcAvatarUrl?.() || mx.getUser(userId)?.avatarUrl; const avatarMxc = roomMember?.getMxcAvatarUrl?.() || mx.getUser(userId)?.avatarUrl;
const 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 powerLevel = roomMember?.powerLevel || 0;
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0; const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0;

View file

@ -15,7 +15,7 @@ export function AuthFooter() {
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
v4.1.0 v4.2.0
</Text> </Text>
<Text as="a" size="T300" href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer"> <Text as="a" size="T300" href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer">
Twitter Twitter

View file

@ -24,7 +24,7 @@ export function WelcomePage() {
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
> >
v4.1.0 v4.2.0
</a> </a>
</span> </span>
} }

View file

@ -273,3 +273,20 @@ export const mxcUrlToHttp = (
allowRedirects, allowRedirects,
useAuthentication 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;
};

View file

@ -1,5 +1,5 @@
const cons = { const cons = {
version: '4.1.0', version: '4.2.0',
secretKey: { secretKey: {
ACCESS_TOKEN: 'cinny_access_token', ACCESS_TOKEN: 'cinny_access_token',
DEVICE_ID: 'cinny_device_id', DEVICE_ID: 'cinny_device_id',