mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-04 22:40:29 +03:00
Merge branch 'dev' into fix-257
This commit is contained in:
commit
f2c5a595b9
15 changed files with 189 additions and 48 deletions
4
.github/workflows/prod-deploy.yml
vendored
4
.github/workflows/prod-deploy.yml
vendored
|
|
@ -52,7 +52,7 @@ jobs:
|
||||||
gpg --export | xxd -p
|
gpg --export | xxd -p
|
||||||
echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
||||||
- name: Upload tagged release
|
- name: Upload tagged release
|
||||||
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8
|
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
||||||
|
|
@ -70,7 +70,7 @@ jobs:
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3.6.0
|
uses: docker/setup-qemu-action@v3.6.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.10.0
|
uses: docker/setup-buildx-action@v3.11.1
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3.5.0
|
uses: docker/login-action@v3.5.0
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
## App
|
## App
|
||||||
FROM nginx:1.29.0-alpine
|
FROM nginx:1.29.1-alpine
|
||||||
|
|
||||||
COPY --from=builder /src/dist /app
|
COPY --from=builder /src/dist /app
|
||||||
COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf
|
COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
|
||||||
25
package-lock.json
generated
25
package-lock.json
generated
|
|
@ -43,7 +43,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": "37.5.0",
|
"matrix-js-sdk": "38.2.0",
|
||||||
"millify": "6.1.0",
|
"millify": "6.1.0",
|
||||||
"pdfjs-dist": "4.2.67",
|
"pdfjs-dist": "4.2.67",
|
||||||
"prismjs": "1.30.0",
|
"prismjs": "1.30.0",
|
||||||
|
|
@ -2256,20 +2256,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
|
"node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
|
||||||
"version": "14.1.0",
|
"version": "15.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-14.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-15.3.0.tgz",
|
||||||
"integrity": "sha512-vcSxHJIr6lP0Fgo8jl0sTHg+OZxZn+skGjiyB62erfgw/R2QqJl0ZVSY8SRcbk9LtHo/ZGld1tnaOyjL2e3cLQ==",
|
"integrity": "sha512-QyxHvncvkl7nf+tnn92PjQ54gMNV8hMSpiukiDgNrqF6IYwgySTlcSdkPYdw8QjZJ0NR6fnVrNzMec0OohM3wA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@matrix-org/olm": {
|
|
||||||
"version": "3.2.15",
|
|
||||||
"resolved": "https://registry.npmjs.org/@matrix-org/olm/-/olm-3.2.15.tgz",
|
|
||||||
"integrity": "sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q==",
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
|
|
@ -8631,14 +8625,13 @@
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/matrix-js-sdk": {
|
"node_modules/matrix-js-sdk": {
|
||||||
"version": "37.5.0",
|
"version": "38.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-37.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-38.2.0.tgz",
|
||||||
"integrity": "sha512-5tyuAi5hnKud1UkVq8Z2/3c22hWGELBZzErJPZkE6Hju2uGUfGtrIx6uj6puv0ZjvsUU3X6Qgm8vdReKO1PGig==",
|
"integrity": "sha512-R3jzK8rDGi/3OXOax8jFFyxblCG9KTT5yuXAbvnZCGcpTm8lZ4mHQAn5UydVD8qiyUMNMpaaMd6/k7N+5I/yaQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@matrix-org/matrix-sdk-crypto-wasm": "^14.0.1",
|
"@matrix-org/matrix-sdk-crypto-wasm": "^15.1.0",
|
||||||
"@matrix-org/olm": "3.2.15",
|
|
||||||
"another-json": "^0.2.0",
|
"another-json": "^0.2.0",
|
||||||
"bs58": "^6.0.0",
|
"bs58": "^6.0.0",
|
||||||
"content-type": "^1.0.4",
|
"content-type": "^1.0.4",
|
||||||
|
|
@ -8653,7 +8646,7 @@
|
||||||
"uuid": "11"
|
"uuid": "11"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=22.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/matrix-js-sdk/node_modules/uuid": {
|
"node_modules/matrix-js-sdk/node_modules/uuid": {
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,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": "37.5.0",
|
"matrix-js-sdk": "38.2.0",
|
||||||
"millify": "6.1.0",
|
"millify": "6.1.0",
|
||||||
"pdfjs-dist": "4.2.67",
|
"pdfjs-dist": "4.2.67",
|
||||||
"prismjs": "1.30.0",
|
"prismjs": "1.30.0",
|
||||||
|
|
|
||||||
|
|
@ -209,13 +209,11 @@ export function RenderMessageContent({
|
||||||
<MVideo
|
<MVideo
|
||||||
content={getContent()}
|
content={getContent()}
|
||||||
renderAsFile={renderFile}
|
renderAsFile={renderFile}
|
||||||
renderVideoContent={({ body, info, mimeType, url, encInfo }) => (
|
renderVideoContent={({ body, info, ...props }) => (
|
||||||
<VideoContent
|
<VideoContent
|
||||||
body={body}
|
body={body}
|
||||||
info={info}
|
info={info}
|
||||||
mimeType={mimeType}
|
{...props}
|
||||||
url={url}
|
|
||||||
encInfo={encInfo}
|
|
||||||
renderThumbnail={
|
renderThumbnail={
|
||||||
mediaAutoLoad
|
mediaAutoLoad
|
||||||
? () => (
|
? () => (
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,8 @@ type RenderVideoContentProps = {
|
||||||
mimeType: string;
|
mimeType: string;
|
||||||
url: string;
|
url: string;
|
||||||
encInfo?: IEncryptedFile;
|
encInfo?: IEncryptedFile;
|
||||||
|
markedAsSpoiler?: boolean;
|
||||||
|
spoilerReason?: string;
|
||||||
};
|
};
|
||||||
type MVideoProps = {
|
type MVideoProps = {
|
||||||
content: IVideoContent;
|
content: IVideoContent;
|
||||||
|
|
@ -274,6 +276,8 @@ export function MVideo({ content, renderAsFile, renderVideoContent, outlined }:
|
||||||
mimeType: safeMimeType,
|
mimeType: safeMimeType,
|
||||||
url: mxcUrl,
|
url: mxcUrl,
|
||||||
encInfo: content.file,
|
encInfo: content.file,
|
||||||
|
markedAsSpoiler: content[MATRIX_SPOILER_PROPERTY_NAME],
|
||||||
|
spoilerReason: content[MATRIX_SPOILER_REASON_PROPERTY_NAME],
|
||||||
})}
|
})}
|
||||||
</AttachmentBox>
|
</AttachmentBox>
|
||||||
</Attachment>
|
</Attachment>
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ export const ImageContent = as<'div', ImageContentProps>(
|
||||||
)}
|
)}
|
||||||
{(srcState.status === AsyncStatus.Loading || srcState.status === AsyncStatus.Success) &&
|
{(srcState.status === AsyncStatus.Loading || srcState.status === AsyncStatus.Success) &&
|
||||||
!load &&
|
!load &&
|
||||||
!markedAsSpoiler && (
|
!blurred && (
|
||||||
<Box className={css.AbsoluteContainer} alignItems="Center" justifyContent="Center">
|
<Box className={css.AbsoluteContainer} alignItems="Center" justifyContent="Center">
|
||||||
<Spinner variant="Secondary" />
|
<Spinner variant="Secondary" />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
Chip,
|
||||||
Icon,
|
Icon,
|
||||||
Icons,
|
Icons,
|
||||||
Spinner,
|
Spinner,
|
||||||
|
|
@ -47,6 +48,8 @@ type VideoContentProps = {
|
||||||
info: IVideoInfo & IThumbnailContent;
|
info: IVideoInfo & IThumbnailContent;
|
||||||
encInfo?: EncryptedAttachmentInfo;
|
encInfo?: EncryptedAttachmentInfo;
|
||||||
autoPlay?: boolean;
|
autoPlay?: boolean;
|
||||||
|
markedAsSpoiler?: boolean;
|
||||||
|
spoilerReason?: string;
|
||||||
renderThumbnail?: () => ReactNode;
|
renderThumbnail?: () => ReactNode;
|
||||||
renderVideo: (props: RenderVideoProps) => ReactNode;
|
renderVideo: (props: RenderVideoProps) => ReactNode;
|
||||||
};
|
};
|
||||||
|
|
@ -60,6 +63,8 @@ export const VideoContent = as<'div', VideoContentProps>(
|
||||||
info,
|
info,
|
||||||
encInfo,
|
encInfo,
|
||||||
autoPlay,
|
autoPlay,
|
||||||
|
markedAsSpoiler,
|
||||||
|
spoilerReason,
|
||||||
renderThumbnail,
|
renderThumbnail,
|
||||||
renderVideo,
|
renderVideo,
|
||||||
...props
|
...props
|
||||||
|
|
@ -72,6 +77,7 @@ export const VideoContent = as<'div', VideoContentProps>(
|
||||||
|
|
||||||
const [load, setLoad] = useState(false);
|
const [load, setLoad] = useState(false);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
|
const [blurred, setBlurred] = useState(markedAsSpoiler ?? false);
|
||||||
|
|
||||||
const [srcState, loadSrc] = useAsyncCallback(
|
const [srcState, loadSrc] = useAsyncCallback(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
|
|
@ -114,11 +120,15 @@ export const VideoContent = as<'div', VideoContentProps>(
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{renderThumbnail && !load && (
|
{renderThumbnail && !load && (
|
||||||
<Box className={css.AbsoluteContainer} alignItems="Center" justifyContent="Center">
|
<Box
|
||||||
|
className={classNames(css.AbsoluteContainer, blurred && css.Blur)}
|
||||||
|
alignItems="Center"
|
||||||
|
justifyContent="Center"
|
||||||
|
>
|
||||||
{renderThumbnail()}
|
{renderThumbnail()}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{!autoPlay && srcState.status === AsyncStatus.Idle && (
|
{!autoPlay && !blurred && srcState.status === AsyncStatus.Idle && (
|
||||||
<Box className={css.AbsoluteContainer} alignItems="Center" justifyContent="Center">
|
<Box className={css.AbsoluteContainer} alignItems="Center" justifyContent="Center">
|
||||||
<Button
|
<Button
|
||||||
variant="Secondary"
|
variant="Secondary"
|
||||||
|
|
@ -133,7 +143,7 @@ export const VideoContent = as<'div', VideoContentProps>(
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{srcState.status === AsyncStatus.Success && (
|
{srcState.status === AsyncStatus.Success && (
|
||||||
<Box className={css.AbsoluteContainer}>
|
<Box className={classNames(css.AbsoluteContainer, blurred && css.Blur)}>
|
||||||
{renderVideo({
|
{renderVideo({
|
||||||
title: body,
|
title: body,
|
||||||
src: srcState.data,
|
src: srcState.data,
|
||||||
|
|
@ -144,8 +154,39 @@ export const VideoContent = as<'div', VideoContentProps>(
|
||||||
})}
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
{blurred && !error && srcState.status !== AsyncStatus.Error && (
|
||||||
|
<Box className={css.AbsoluteContainer} alignItems="Center" justifyContent="Center">
|
||||||
|
<TooltipProvider
|
||||||
|
tooltip={
|
||||||
|
typeof spoilerReason === 'string' && (
|
||||||
|
<Tooltip variant="Secondary">
|
||||||
|
<Text>{spoilerReason}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
position="Top"
|
||||||
|
align="Center"
|
||||||
|
>
|
||||||
|
{(triggerRef) => (
|
||||||
|
<Chip
|
||||||
|
ref={triggerRef}
|
||||||
|
variant="Secondary"
|
||||||
|
radii="Pill"
|
||||||
|
size="500"
|
||||||
|
outlined
|
||||||
|
onClick={() => {
|
||||||
|
setBlurred(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="B300">Spoiler</Text>
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
{(srcState.status === AsyncStatus.Loading || srcState.status === AsyncStatus.Success) &&
|
{(srcState.status === AsyncStatus.Loading || srcState.status === AsyncStatus.Success) &&
|
||||||
!load && (
|
!load &&
|
||||||
|
!blurred && (
|
||||||
<Box className={css.AbsoluteContainer} alignItems="Center" justifyContent="Center">
|
<Box className={css.AbsoluteContainer} alignItems="Center" justifyContent="Center">
|
||||||
<Spinner variant="Secondary" />
|
<Spinner variant="Secondary" />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { ReactNode, useEffect } from 'react';
|
||||||
import { Box, Chip, Icon, IconButton, Icons, Text, color, config, toRem } from 'folds';
|
import { Box, Chip, Icon, IconButton, Icons, Text, color, config, toRem } from 'folds';
|
||||||
import { UploadCard, UploadCardError, UploadCardProgress } from './UploadCard';
|
import { UploadCard, UploadCardError, UploadCardProgress } from './UploadCard';
|
||||||
import { UploadStatus, UploadSuccess, useBindUploadAtom } from '../../state/upload';
|
import { UploadStatus, UploadSuccess, useBindUploadAtom } from '../../state/upload';
|
||||||
|
|
@ -13,8 +13,54 @@ import {
|
||||||
import { useObjectURL } from '../../hooks/useObjectURL';
|
import { useObjectURL } from '../../hooks/useObjectURL';
|
||||||
import { useMediaConfig } from '../../hooks/useMediaConfig';
|
import { useMediaConfig } from '../../hooks/useMediaConfig';
|
||||||
|
|
||||||
type ImagePreviewProps = { fileItem: TUploadItem; onSpoiler: (marked: boolean) => void };
|
type PreviewImageProps = {
|
||||||
function ImagePreview({ fileItem, onSpoiler }: ImagePreviewProps) {
|
fileItem: TUploadItem;
|
||||||
|
};
|
||||||
|
function PreviewImage({ fileItem }: PreviewImageProps) {
|
||||||
|
const { originalFile, metadata } = fileItem;
|
||||||
|
const fileUrl = useObjectURL(originalFile);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
style={{
|
||||||
|
objectFit: 'contain',
|
||||||
|
width: '100%',
|
||||||
|
height: toRem(152),
|
||||||
|
filter: metadata.markedAsSpoiler ? 'blur(44px)' : undefined,
|
||||||
|
}}
|
||||||
|
alt={originalFile.name}
|
||||||
|
src={fileUrl}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type PreviewVideoProps = {
|
||||||
|
fileItem: TUploadItem;
|
||||||
|
};
|
||||||
|
function PreviewVideo({ fileItem }: PreviewVideoProps) {
|
||||||
|
const { originalFile, metadata } = fileItem;
|
||||||
|
const fileUrl = useObjectURL(originalFile);
|
||||||
|
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line jsx-a11y/media-has-caption
|
||||||
|
<video
|
||||||
|
style={{
|
||||||
|
objectFit: 'contain',
|
||||||
|
width: '100%',
|
||||||
|
height: toRem(152),
|
||||||
|
filter: metadata.markedAsSpoiler ? 'blur(44px)' : undefined,
|
||||||
|
}}
|
||||||
|
src={fileUrl}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type MediaPreviewProps = {
|
||||||
|
fileItem: TUploadItem;
|
||||||
|
onSpoiler: (marked: boolean) => void;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
function MediaPreview({ fileItem, onSpoiler, children }: MediaPreviewProps) {
|
||||||
const { originalFile, metadata } = fileItem;
|
const { originalFile, metadata } = fileItem;
|
||||||
const fileUrl = useObjectURL(originalFile);
|
const fileUrl = useObjectURL(originalFile);
|
||||||
|
|
||||||
|
|
@ -27,16 +73,7 @@ function ImagePreview({ fileItem, onSpoiler }: ImagePreviewProps) {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
{children}
|
||||||
style={{
|
|
||||||
objectFit: 'contain',
|
|
||||||
width: '100%',
|
|
||||||
height: toRem(152),
|
|
||||||
filter: fileItem.metadata.markedAsSpoiler ? 'blur(44px)' : undefined,
|
|
||||||
}}
|
|
||||||
src={fileUrl}
|
|
||||||
alt={originalFile.name}
|
|
||||||
/>
|
|
||||||
<Box
|
<Box
|
||||||
justifyContent="End"
|
justifyContent="End"
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -136,7 +173,14 @@ export function UploadCardRenderer({
|
||||||
bottom={
|
bottom={
|
||||||
<>
|
<>
|
||||||
{fileItem.originalFile.type.startsWith('image') && (
|
{fileItem.originalFile.type.startsWith('image') && (
|
||||||
<ImagePreview fileItem={fileItem} onSpoiler={handleSpoiler} />
|
<MediaPreview fileItem={fileItem} onSpoiler={handleSpoiler}>
|
||||||
|
<PreviewImage fileItem={fileItem} />
|
||||||
|
</MediaPreview>
|
||||||
|
)}
|
||||||
|
{fileItem.originalFile.type.startsWith('video') && (
|
||||||
|
<MediaPreview fileItem={fileItem} onSpoiler={handleSpoiler}>
|
||||||
|
<PreviewVideo fileItem={fileItem} />
|
||||||
|
</MediaPreview>
|
||||||
)}
|
)}
|
||||||
{upload.status === UploadStatus.Idle && !fileSizeExceeded && (
|
{upload.status === UploadStatus.Idle && !fileSizeExceeded && (
|
||||||
<UploadCardProgress sentBytes={0} totalBytes={file.size} />
|
<UploadCardProgress sentBytes={0} totalBytes={file.size} />
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||||
import { useTheme } from '../../hooks/useTheme';
|
import { useTheme } from '../../hooks/useTheme';
|
||||||
import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
|
import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
|
||||||
import { usePowerLevelTags } from '../../hooks/usePowerLevelTags';
|
import { usePowerLevelTags } from '../../hooks/usePowerLevelTags';
|
||||||
|
import { useComposingCheck } from '../../hooks/useComposingCheck';
|
||||||
|
|
||||||
interface RoomInputProps {
|
interface RoomInputProps {
|
||||||
editor: Editor;
|
editor: Editor;
|
||||||
|
|
@ -217,6 +218,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
const dropZoneVisible = useFileDropZone(fileDropContainerRef, handleFiles);
|
const dropZoneVisible = useFileDropZone(fileDropContainerRef, handleFiles);
|
||||||
const [hideStickerBtn, setHideStickerBtn] = useState(document.body.clientWidth < 500);
|
const [hideStickerBtn, setHideStickerBtn] = useState(document.body.clientWidth < 500);
|
||||||
|
|
||||||
|
const isComposing = useComposingCheck();
|
||||||
|
|
||||||
useElementSizeObserver(
|
useElementSizeObserver(
|
||||||
useCallback(() => document.body, []),
|
useCallback(() => document.body, []),
|
||||||
useCallback((width) => setHideStickerBtn(width < 500), [])
|
useCallback((width) => setHideStickerBtn(width < 500), [])
|
||||||
|
|
@ -380,7 +383,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
(evt) => {
|
(evt) => {
|
||||||
if (
|
if (
|
||||||
(isKeyHotkey('mod+enter', evt) || (!enterForNewline && isKeyHotkey('enter', evt))) &&
|
(isKeyHotkey('mod+enter', evt) || (!enterForNewline && isKeyHotkey('enter', evt))) &&
|
||||||
!evt.nativeEvent.isComposing
|
!isComposing(evt)
|
||||||
) {
|
) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
submit();
|
submit();
|
||||||
|
|
@ -394,7 +397,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
setReplyDraft(undefined);
|
setReplyDraft(undefined);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[submit, setReplyDraft, enterForNewline, autocompleteQuery]
|
[submit, setReplyDraft, enterForNewline, autocompleteQuery, isComposing]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleKeyUp: KeyboardEventHandler = useCallback(
|
const handleKeyUp: KeyboardEventHandler = useCallback(
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { getEditedEvent, getMentionContent, trimReplyFromFormattedBody } from '../../../utils/room';
|
import { getEditedEvent, getMentionContent, trimReplyFromFormattedBody } from '../../../utils/room';
|
||||||
import { mobileOrTablet } from '../../../utils/user-agent';
|
import { mobileOrTablet } from '../../../utils/user-agent';
|
||||||
|
import { useComposingCheck } from '../../../hooks/useComposingCheck';
|
||||||
|
|
||||||
type MessageEditorProps = {
|
type MessageEditorProps = {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
|
|
@ -69,6 +70,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
|
||||||
const [globalToolbar] = useSetting(settingsAtom, 'editorToolbar');
|
const [globalToolbar] = useSetting(settingsAtom, 'editorToolbar');
|
||||||
const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown');
|
const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown');
|
||||||
const [toolbar, setToolbar] = useState(globalToolbar);
|
const [toolbar, setToolbar] = useState(globalToolbar);
|
||||||
|
const isComposing = useComposingCheck();
|
||||||
|
|
||||||
const [autocompleteQuery, setAutocompleteQuery] =
|
const [autocompleteQuery, setAutocompleteQuery] =
|
||||||
useState<AutocompleteQuery<AutocompletePrefix>>();
|
useState<AutocompleteQuery<AutocompletePrefix>>();
|
||||||
|
|
@ -163,7 +165,10 @@ export const MessageEditor = as<'div', MessageEditorProps>(
|
||||||
|
|
||||||
const handleKeyDown: KeyboardEventHandler = useCallback(
|
const handleKeyDown: KeyboardEventHandler = useCallback(
|
||||||
(evt) => {
|
(evt) => {
|
||||||
if ((isKeyHotkey('mod+enter', evt) || (!enterForNewline && isKeyHotkey('enter', evt))) && !evt.nativeEvent.isComposing) {
|
if (
|
||||||
|
(isKeyHotkey('mod+enter', evt) || (!enterForNewline && isKeyHotkey('enter', evt))) &&
|
||||||
|
!isComposing(evt)
|
||||||
|
) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
handleSave();
|
handleSave();
|
||||||
}
|
}
|
||||||
|
|
@ -172,7 +177,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
|
||||||
onCancel();
|
onCancel();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onCancel, handleSave, enterForNewline]
|
[onCancel, handleSave, enterForNewline, isComposing]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleKeyUp: KeyboardEventHandler = useCallback(
|
const handleKeyUp: KeyboardEventHandler = useCallback(
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ export const getVideoMsgContent = async (
|
||||||
item: TUploadItem,
|
item: TUploadItem,
|
||||||
mxc: string
|
mxc: string
|
||||||
): Promise<IContent> => {
|
): Promise<IContent> => {
|
||||||
const { file, originalFile, encInfo } = item;
|
const { file, originalFile, encInfo, metadata } = item;
|
||||||
|
|
||||||
const [videoError, videoEl] = await to(loadVideoElement(getVideoFileUrl(originalFile)));
|
const [videoError, videoEl] = await to(loadVideoElement(getVideoFileUrl(originalFile)));
|
||||||
if (videoError) console.warn(videoError);
|
if (videoError) console.warn(videoError);
|
||||||
|
|
@ -91,6 +91,7 @@ export const getVideoMsgContent = async (
|
||||||
msgtype: MsgType.Video,
|
msgtype: MsgType.Video,
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
body: file.name,
|
body: file.name,
|
||||||
|
[MATRIX_SPOILER_PROPERTY_NAME]: metadata.markedAsSpoiler,
|
||||||
};
|
};
|
||||||
if (videoEl) {
|
if (videoEl) {
|
||||||
const [thumbError, thumbContent] = await to(
|
const [thumbError, thumbContent] = await to(
|
||||||
|
|
|
||||||
47
src/app/hooks/useComposingCheck.ts
Normal file
47
src/app/hooks/useComposingCheck.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
|
import { lastCompositionEndAtom } from '../state/lastCompositionEnd';
|
||||||
|
|
||||||
|
interface TimeStamped {
|
||||||
|
readonly timeStamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCompositionEndTracking(): void {
|
||||||
|
const setLastCompositionEnd = useSetAtom(lastCompositionEndAtom);
|
||||||
|
|
||||||
|
const recordCompositionEnd = useCallback(
|
||||||
|
(evt: TimeStamped) => {
|
||||||
|
setLastCompositionEnd(evt.timeStamp);
|
||||||
|
},
|
||||||
|
[setLastCompositionEnd]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('compositionend', recordCompositionEnd, { capture: true });
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('compositionend', recordCompositionEnd, { capture: true });
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IsComposingLike {
|
||||||
|
readonly timeStamp: number;
|
||||||
|
readonly keyCode: number;
|
||||||
|
readonly nativeEvent: {
|
||||||
|
readonly isComposing?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useComposingCheck({
|
||||||
|
compositionEndThreshold = 500,
|
||||||
|
}: { compositionEndThreshold?: number } = {}): (evt: IsComposingLike) => boolean {
|
||||||
|
const compositionEnd = useAtomValue(lastCompositionEndAtom);
|
||||||
|
return useCallback(
|
||||||
|
(evt: IsComposingLike): boolean =>
|
||||||
|
evt.nativeEvent.isComposing ||
|
||||||
|
(evt.keyCode === 229 &&
|
||||||
|
typeof compositionEnd !== 'undefined' &&
|
||||||
|
evt.timeStamp - compositionEnd < compositionEndThreshold),
|
||||||
|
[compositionEndThreshold, compositionEnd]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -11,11 +11,13 @@ import { ConfigConfigError, ConfigConfigLoading } from './ConfigConfig';
|
||||||
import { FeatureCheck } from './FeatureCheck';
|
import { FeatureCheck } from './FeatureCheck';
|
||||||
import { createRouter } from './Router';
|
import { createRouter } from './Router';
|
||||||
import { ScreenSizeProvider, useScreenSize } from '../hooks/useScreenSize';
|
import { ScreenSizeProvider, useScreenSize } from '../hooks/useScreenSize';
|
||||||
|
import { useCompositionEndTracking } from '../hooks/useComposingCheck';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const screenSize = useScreenSize();
|
const screenSize = useScreenSize();
|
||||||
|
useCompositionEndTracking();
|
||||||
|
|
||||||
const portalContainer = document.getElementById('portalContainer') ?? undefined;
|
const portalContainer = document.getElementById('portalContainer') ?? undefined;
|
||||||
|
|
||||||
|
|
|
||||||
3
src/app/state/lastCompositionEnd.ts
Normal file
3
src/app/state/lastCompositionEnd.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
|
export const lastCompositionEndAtom = atom<number | undefined>(undefined);
|
||||||
Loading…
Add table
Add a link
Reference in a new issue