Merge branch 'dev' into fix-257

This commit is contained in:
Ajay Bura 2025-09-27 10:00:30 +05:30 committed by GitHub
commit f2c5a595b9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 189 additions and 48 deletions

View file

@ -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:

View file

@ -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
View file

@ -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": {

View file

@ -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",

View file

@ -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
? () => ( ? () => (

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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} />

View file

@ -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(

View file

@ -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(

View file

@ -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(

View 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]
);
}

View file

@ -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;

View file

@ -0,0 +1,3 @@
import { atom } from 'jotai';
export const lastCompositionEndAtom = atom<number | undefined>(undefined);