mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-05 06:50:28 +03:00
Merge branch 'dev' into msc4133
This commit is contained in:
commit
4e7b64eb5f
18 changed files with 195 additions and 54 deletions
4
.github/workflows/prod-deploy.yml
vendored
4
.github/workflows/prod-deploy.yml
vendored
|
|
@ -52,7 +52,7 @@ jobs:
|
|||
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
|
||||
- name: Upload tagged release
|
||||
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8
|
||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
|
||||
with:
|
||||
files: |
|
||||
cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
||||
|
|
@ -70,7 +70,7 @@ jobs:
|
|||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.6.0
|
||||
- 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
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ RUN npm run build
|
|||
|
||||
|
||||
## App
|
||||
FROM nginx:1.29.0-alpine
|
||||
FROM nginx:1.29.1-alpine
|
||||
|
||||
COPY --from=builder /src/dist /app
|
||||
COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
|
|
|||
29
package-lock.json
generated
29
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "cinny",
|
||||
"version": "4.10.0",
|
||||
"version": "4.10.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cinny",
|
||||
"version": "4.10.0",
|
||||
"version": "4.10.1",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "1.1.6",
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
"jotai": "2.6.0",
|
||||
"linkify-react": "4.1.3",
|
||||
"linkifyjs": "4.1.3",
|
||||
"matrix-js-sdk": "37.5.0",
|
||||
"matrix-js-sdk": "38.2.0",
|
||||
"millify": "6.1.0",
|
||||
"pdfjs-dist": "4.2.67",
|
||||
"prismjs": "1.30.0",
|
||||
|
|
@ -2257,20 +2257,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-14.1.0.tgz",
|
||||
"integrity": "sha512-vcSxHJIr6lP0Fgo8jl0sTHg+OZxZn+skGjiyB62erfgw/R2QqJl0ZVSY8SRcbk9LtHo/ZGld1tnaOyjL2e3cLQ==",
|
||||
"version": "15.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-15.3.0.tgz",
|
||||
"integrity": "sha512-QyxHvncvkl7nf+tnn92PjQ54gMNV8hMSpiukiDgNrqF6IYwgySTlcSdkPYdw8QjZJ0NR6fnVrNzMec0OohM3wA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"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": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
|
@ -8632,14 +8626,13 @@
|
|||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/matrix-js-sdk": {
|
||||
"version": "37.5.0",
|
||||
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-37.5.0.tgz",
|
||||
"integrity": "sha512-5tyuAi5hnKud1UkVq8Z2/3c22hWGELBZzErJPZkE6Hju2uGUfGtrIx6uj6puv0ZjvsUU3X6Qgm8vdReKO1PGig==",
|
||||
"version": "38.2.0",
|
||||
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-38.2.0.tgz",
|
||||
"integrity": "sha512-R3jzK8rDGi/3OXOax8jFFyxblCG9KTT5yuXAbvnZCGcpTm8lZ4mHQAn5UydVD8qiyUMNMpaaMd6/k7N+5I/yaQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^14.0.1",
|
||||
"@matrix-org/olm": "3.2.15",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^15.1.0",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^6.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
|
|
@ -8654,7 +8647,7 @@
|
|||
"uuid": "11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/matrix-js-sdk/node_modules/uuid": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "cinny",
|
||||
"version": "4.10.0",
|
||||
"version": "4.10.1",
|
||||
"description": "Yet another matrix client",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
"jotai": "2.6.0",
|
||||
"linkify-react": "4.1.3",
|
||||
"linkifyjs": "4.1.3",
|
||||
"matrix-js-sdk": "37.5.0",
|
||||
"matrix-js-sdk": "38.2.0",
|
||||
"millify": "6.1.0",
|
||||
"pdfjs-dist": "4.2.67",
|
||||
"prismjs": "1.30.0",
|
||||
|
|
|
|||
|
|
@ -209,13 +209,11 @@ export function RenderMessageContent({
|
|||
<MVideo
|
||||
content={getContent()}
|
||||
renderAsFile={renderFile}
|
||||
renderVideoContent={({ body, info, mimeType, url, encInfo }) => (
|
||||
renderVideoContent={({ body, info, ...props }) => (
|
||||
<VideoContent
|
||||
body={body}
|
||||
info={info}
|
||||
mimeType={mimeType}
|
||||
url={url}
|
||||
encInfo={encInfo}
|
||||
{...props}
|
||||
renderThumbnail={
|
||||
mediaAutoLoad
|
||||
? () => (
|
||||
|
|
|
|||
|
|
@ -224,6 +224,8 @@ type RenderVideoContentProps = {
|
|||
mimeType: string;
|
||||
url: string;
|
||||
encInfo?: IEncryptedFile;
|
||||
markedAsSpoiler?: boolean;
|
||||
spoilerReason?: string;
|
||||
};
|
||||
type MVideoProps = {
|
||||
content: IVideoContent;
|
||||
|
|
@ -274,6 +276,8 @@ export function MVideo({ content, renderAsFile, renderVideoContent, outlined }:
|
|||
mimeType: safeMimeType,
|
||||
url: mxcUrl,
|
||||
encInfo: content.file,
|
||||
markedAsSpoiler: content[MATRIX_SPOILER_PROPERTY_NAME],
|
||||
spoilerReason: content[MATRIX_SPOILER_REASON_PROPERTY_NAME],
|
||||
})}
|
||||
</AttachmentBox>
|
||||
</Attachment>
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ export const ImageContent = as<'div', ImageContentProps>(
|
|||
)}
|
||||
{(srcState.status === AsyncStatus.Loading || srcState.status === AsyncStatus.Success) &&
|
||||
!load &&
|
||||
!markedAsSpoiler && (
|
||||
!blurred && (
|
||||
<Box className={css.AbsoluteContainer} alignItems="Center" justifyContent="Center">
|
||||
<Spinner variant="Secondary" />
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
Icon,
|
||||
Icons,
|
||||
Spinner,
|
||||
|
|
@ -47,6 +48,8 @@ type VideoContentProps = {
|
|||
info: IVideoInfo & IThumbnailContent;
|
||||
encInfo?: EncryptedAttachmentInfo;
|
||||
autoPlay?: boolean;
|
||||
markedAsSpoiler?: boolean;
|
||||
spoilerReason?: string;
|
||||
renderThumbnail?: () => ReactNode;
|
||||
renderVideo: (props: RenderVideoProps) => ReactNode;
|
||||
};
|
||||
|
|
@ -60,6 +63,8 @@ export const VideoContent = as<'div', VideoContentProps>(
|
|||
info,
|
||||
encInfo,
|
||||
autoPlay,
|
||||
markedAsSpoiler,
|
||||
spoilerReason,
|
||||
renderThumbnail,
|
||||
renderVideo,
|
||||
...props
|
||||
|
|
@ -72,6 +77,7 @@ export const VideoContent = as<'div', VideoContentProps>(
|
|||
|
||||
const [load, setLoad] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const [blurred, setBlurred] = useState(markedAsSpoiler ?? false);
|
||||
|
||||
const [srcState, loadSrc] = useAsyncCallback(
|
||||
useCallback(async () => {
|
||||
|
|
@ -114,11 +120,15 @@ export const VideoContent = as<'div', VideoContentProps>(
|
|||
/>
|
||||
)}
|
||||
{renderThumbnail && !load && (
|
||||
<Box className={css.AbsoluteContainer} alignItems="Center" justifyContent="Center">
|
||||
<Box
|
||||
className={classNames(css.AbsoluteContainer, blurred && css.Blur)}
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
>
|
||||
{renderThumbnail()}
|
||||
</Box>
|
||||
)}
|
||||
{!autoPlay && srcState.status === AsyncStatus.Idle && (
|
||||
{!autoPlay && !blurred && srcState.status === AsyncStatus.Idle && (
|
||||
<Box className={css.AbsoluteContainer} alignItems="Center" justifyContent="Center">
|
||||
<Button
|
||||
variant="Secondary"
|
||||
|
|
@ -133,7 +143,7 @@ export const VideoContent = as<'div', VideoContentProps>(
|
|||
</Box>
|
||||
)}
|
||||
{srcState.status === AsyncStatus.Success && (
|
||||
<Box className={css.AbsoluteContainer}>
|
||||
<Box className={classNames(css.AbsoluteContainer, blurred && css.Blur)}>
|
||||
{renderVideo({
|
||||
title: body,
|
||||
src: srcState.data,
|
||||
|
|
@ -144,8 +154,39 @@ export const VideoContent = as<'div', VideoContentProps>(
|
|||
})}
|
||||
</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) &&
|
||||
!load && (
|
||||
!load &&
|
||||
!blurred && (
|
||||
<Box className={css.AbsoluteContainer} alignItems="Center" justifyContent="Center">
|
||||
<Spinner variant="Secondary" />
|
||||
</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 { UploadCard, UploadCardError, UploadCardProgress } from './UploadCard';
|
||||
import { UploadStatus, UploadSuccess, useBindUploadAtom } from '../../state/upload';
|
||||
|
|
@ -13,8 +13,54 @@ import {
|
|||
import { useObjectURL } from '../../hooks/useObjectURL';
|
||||
import { useMediaConfig } from '../../hooks/useMediaConfig';
|
||||
|
||||
type ImagePreviewProps = { fileItem: TUploadItem; onSpoiler: (marked: boolean) => void };
|
||||
function ImagePreview({ fileItem, onSpoiler }: ImagePreviewProps) {
|
||||
type PreviewImageProps = {
|
||||
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 fileUrl = useObjectURL(originalFile);
|
||||
|
||||
|
|
@ -27,16 +73,7 @@ function ImagePreview({ fileItem, onSpoiler }: ImagePreviewProps) {
|
|||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
objectFit: 'contain',
|
||||
width: '100%',
|
||||
height: toRem(152),
|
||||
filter: fileItem.metadata.markedAsSpoiler ? 'blur(44px)' : undefined,
|
||||
}}
|
||||
src={fileUrl}
|
||||
alt={originalFile.name}
|
||||
/>
|
||||
{children}
|
||||
<Box
|
||||
justifyContent="End"
|
||||
style={{
|
||||
|
|
@ -136,7 +173,14 @@ export function UploadCardRenderer({
|
|||
bottom={
|
||||
<>
|
||||
{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 && (
|
||||
<UploadCardProgress sentBytes={0} totalBytes={file.size} />
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ import { useRoomCreators } from '../../hooks/useRoomCreators';
|
|||
import { useTheme } from '../../hooks/useTheme';
|
||||
import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
|
||||
import { usePowerLevelTags } from '../../hooks/usePowerLevelTags';
|
||||
import { useComposingCheck } from '../../hooks/useComposingCheck';
|
||||
|
||||
interface RoomInputProps {
|
||||
editor: Editor;
|
||||
|
|
@ -217,6 +218,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
const dropZoneVisible = useFileDropZone(fileDropContainerRef, handleFiles);
|
||||
const [hideStickerBtn, setHideStickerBtn] = useState(document.body.clientWidth < 500);
|
||||
|
||||
const isComposing = useComposingCheck();
|
||||
|
||||
useElementSizeObserver(
|
||||
useCallback(() => document.body, []),
|
||||
useCallback((width) => setHideStickerBtn(width < 500), [])
|
||||
|
|
@ -380,7 +383,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
(evt) => {
|
||||
if (
|
||||
(isKeyHotkey('mod+enter', evt) || (!enterForNewline && isKeyHotkey('enter', evt))) &&
|
||||
!evt.nativeEvent.isComposing
|
||||
!isComposing(evt)
|
||||
) {
|
||||
evt.preventDefault();
|
||||
submit();
|
||||
|
|
@ -394,7 +397,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
setReplyDraft(undefined);
|
||||
}
|
||||
},
|
||||
[submit, setReplyDraft, enterForNewline, autocompleteQuery]
|
||||
[submit, setReplyDraft, enterForNewline, autocompleteQuery, isComposing]
|
||||
);
|
||||
|
||||
const handleKeyUp: KeyboardEventHandler = useCallback(
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
|||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { getEditedEvent, getMentionContent, trimReplyFromFormattedBody } from '../../../utils/room';
|
||||
import { mobileOrTablet } from '../../../utils/user-agent';
|
||||
import { useComposingCheck } from '../../../hooks/useComposingCheck';
|
||||
|
||||
type MessageEditorProps = {
|
||||
roomId: string;
|
||||
|
|
@ -69,6 +70,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
|
|||
const [globalToolbar] = useSetting(settingsAtom, 'editorToolbar');
|
||||
const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown');
|
||||
const [toolbar, setToolbar] = useState(globalToolbar);
|
||||
const isComposing = useComposingCheck();
|
||||
|
||||
const [autocompleteQuery, setAutocompleteQuery] =
|
||||
useState<AutocompleteQuery<AutocompletePrefix>>();
|
||||
|
|
@ -163,7 +165,10 @@ export const MessageEditor = as<'div', MessageEditorProps>(
|
|||
|
||||
const handleKeyDown: KeyboardEventHandler = useCallback(
|
||||
(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();
|
||||
handleSave();
|
||||
}
|
||||
|
|
@ -172,7 +177,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
|
|||
onCancel();
|
||||
}
|
||||
},
|
||||
[onCancel, handleSave, enterForNewline]
|
||||
[onCancel, handleSave, enterForNewline, isComposing]
|
||||
);
|
||||
|
||||
const handleKeyUp: KeyboardEventHandler = useCallback(
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export const getVideoMsgContent = async (
|
|||
item: TUploadItem,
|
||||
mxc: string
|
||||
): Promise<IContent> => {
|
||||
const { file, originalFile, encInfo } = item;
|
||||
const { file, originalFile, encInfo, metadata } = item;
|
||||
|
||||
const [videoError, videoEl] = await to(loadVideoElement(getVideoFileUrl(originalFile)));
|
||||
if (videoError) console.warn(videoError);
|
||||
|
|
@ -91,6 +91,7 @@ export const getVideoMsgContent = async (
|
|||
msgtype: MsgType.Video,
|
||||
filename: file.name,
|
||||
body: file.name,
|
||||
[MATRIX_SPOILER_PROPERTY_NAME]: metadata.markedAsSpoiler,
|
||||
};
|
||||
if (videoEl) {
|
||||
const [thumbError, thumbContent] = await to(
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export function About({ requestClose }: AboutProps) {
|
|||
<Box direction="Column" gap="100">
|
||||
<Box gap="100" alignItems="End">
|
||||
<Text size="H3">Cinny</Text>
|
||||
<Text size="T200">v4.10.0</Text>
|
||||
<Text size="T200">v4.10.1</Text>
|
||||
</Box>
|
||||
<Text>Yet another matrix client.</Text>
|
||||
</Box>
|
||||
|
|
|
|||
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 { createRouter } from './Router';
|
||||
import { ScreenSizeProvider, useScreenSize } from '../hooks/useScreenSize';
|
||||
import { useCompositionEndTracking } from '../hooks/useComposingCheck';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
function App() {
|
||||
const screenSize = useScreenSize();
|
||||
useCompositionEndTracking();
|
||||
|
||||
const portalContainer = document.getElementById('portalContainer') ?? undefined;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export function AuthFooter() {
|
|||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
v4.10.0
|
||||
v4.10.1
|
||||
</Text>
|
||||
<Text as="a" size="T300" href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer">
|
||||
Twitter
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export function WelcomePage() {
|
|||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
v4.10.0
|
||||
v4.10.1
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
|
|
|
|||
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