mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 06:20:28 +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
 | 
			
		||||
          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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										25
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										25
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -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",
 | 
			
		||||
| 
						 | 
				
			
			@ -2256,20 +2256,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",
 | 
			
		||||
| 
						 | 
				
			
			@ -8631,14 +8625,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",
 | 
			
		||||
| 
						 | 
				
			
			@ -8653,7 +8646,7 @@
 | 
			
		|||
        "uuid": "11"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=20.0.0"
 | 
			
		||||
        "node": ">=22.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/matrix-js-sdk/node_modules/uuid": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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