mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 06:20:28 +03:00 
			
		
		
		
	* rework general settings * account settings - WIP * add missing key prop * add object url hook * extract wide modal styles * profile settings and image editor - WIP * add outline style to upload card * remove file param from bind upload atom hook * add compact variant to upload card * add compact upload card renderer * add option to update profile avatar * add option to change profile displayname * allow displayname change based on capabilities check * rearrange settings components into folders * add system notification settings * add initial page param in settings * convert account data hook to typescript * add push rule hook * add notification mode hook * add notification mode switcher component * add all messages notification settings options * add special messages notification settings * add keyword notifications * add ignored users section * improve ignore user list strings * add about settings * add access token option in about settings * add developer tools settings * add expand button to account data dev tool option * update folds * fix editable active element textarea check * do not close dialog when editable element in focus * add text area plugins * add text area intent handler hook * add newline intent mod in text area * add next line hotkey in text area intent hook * add syntax error position dom utility function * add account data editor * add button to send new account data in dev tools * improve custom emoji plugin * add more custom emojis hooks * add text util css * add word break in setting tile title and description * emojis and sticker user settings - WIP * view image packs from settings * emoji pack editing - WIP * add option to edit pack meta * change saved changes message * add image edit and delete controls * add option to upload pack images and apply changes * fix state event type when updating image pack * lazy load pack image tile img * hide upload image button when user can not edit pack * add option to add or remove global image packs * upgrade to rust crypto (#2168) * update matrix js sdk * remove dead code * use rust crypto * update setPowerLevel usage * fix types * fix deprecated isRoomEncrypted method uses * fix deprecated room.currentState uses * fix deprecated import/export room keys func * fix merge issues in image pack file * fix remaining issues in image pack file * start indexedDBStore * update package lock and vite-plugin-top-level-await * user session settings - WIP * add useAsync hook * add password stage uia * add uia flow matrix error hook * add UIA action component * add options to delete sessions * add sso uia stage * fix SSO stage complete error * encryption - WIP * update user settings encryption terminology * add default variant to password input * use password input in uia password stage * add options for local backup in user settings * remove typo in import local backup password input label * online backup - WIP * fix uia sso action * move access token settings from about to developer tools * merge encryption tab into sessions and rename it to devices * add device placeholder tile * add logout dialog * add logout button for current device * move other devices in component * render unverified device verification tile * add learn more section for current device verification * add device verification status badge * add info card component * add index file for password input component * add types for secret storage * add component to access secret storage key * manual verification - WIP * update matrix-js-sdk to v35 * add manual verification * use react query for device list * show unverified tab on sidebar * fix device list updates * add session key details to current device * render restore encryption backup * fix loading state of restore backup * fix unverified tab settings closes after verification * key backup tile - WIP * fix unverified tab badge * rename session key to device key in device tile * improve backup restore functionality * fix restore button enabled after layout reload during restoring backup * update backup info on status change * add backup disconnection failures * add device verification using sas * restore backup after verification * show option to logout on startup error screen * fix key backup hook update on decryption key cached * add option to enable device verification * add device verification reset dialog * add logout button in settings drawer * add encrypted message lost on logout * fix backup restore never finish with 0 keys * fix setup dialog hides when enabling device verification * show backup details in menu * update setup device verification body copy * replace deprecated method * fix displayname appear as mxid in settings * remove old refactored codes * fix types
		
			
				
	
	
		
			388 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			388 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import React, { useCallback, useEffect, useMemo, useState } from 'react';
 | 
						|
import { as, Box, Text, config, Button, Menu, Spinner } from 'folds';
 | 
						|
import {
 | 
						|
  ImagePack,
 | 
						|
  ImageUsage,
 | 
						|
  PackContent,
 | 
						|
  PackImage,
 | 
						|
  PackImageReader,
 | 
						|
  packMetaEqual,
 | 
						|
  PackMetaReader,
 | 
						|
} from '../../plugins/custom-emoji';
 | 
						|
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
 | 
						|
import { SequenceCard } from '../sequence-card';
 | 
						|
import { ImageTile, ImageTileEdit, ImageTileUpload } from './ImageTile';
 | 
						|
import { SettingTile } from '../setting-tile';
 | 
						|
import { UsageSwitcher } from './UsageSwitcher';
 | 
						|
import { ImagePackProfile, ImagePackProfileEdit } from './PackMeta';
 | 
						|
import * as css from './style.css';
 | 
						|
import { useFilePicker } from '../../hooks/useFilePicker';
 | 
						|
import { CompactUploadCardRenderer } from '../upload-card';
 | 
						|
import { UploadSuccess } from '../../state/upload';
 | 
						|
import { getImageInfo, TUploadContent } from '../../utils/matrix';
 | 
						|
import { getImageFileUrl, loadImageElement, renameFile } from '../../utils/dom';
 | 
						|
import { replaceSpaceWithDash, suffixRename } from '../../utils/common';
 | 
						|
import { getFileNameWithoutExt } from '../../utils/mimeTypes';
 | 
						|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
 | 
						|
 | 
						|
export type ImagePackContentProps = {
 | 
						|
  imagePack: ImagePack;
 | 
						|
  canEdit?: boolean;
 | 
						|
  onUpdate?: (packContent: PackContent) => Promise<void>;
 | 
						|
};
 | 
						|
 | 
						|
export const ImagePackContent = as<'div', ImagePackContentProps>(
 | 
						|
  ({ imagePack, canEdit, onUpdate, ...props }, ref) => {
 | 
						|
    const useAuthentication = useMediaAuthentication();
 | 
						|
 | 
						|
    const [metaEditing, setMetaEditing] = useState(false);
 | 
						|
    const [savedMeta, setSavedMeta] = useState<PackMetaReader>();
 | 
						|
    const currentMeta = savedMeta ?? imagePack.meta;
 | 
						|
 | 
						|
    const images = useMemo(() => Array.from(imagePack.images.collection.values()), [imagePack]);
 | 
						|
    const [files, setFiles] = useState<File[]>([]);
 | 
						|
    const [uploadedImages, setUploadedImages] = useState<PackImageReader[]>([]);
 | 
						|
    const [imagesEditing, setImagesEditing] = useState<Set<string>>(new Set());
 | 
						|
    const [savedImages, setSavedImages] = useState<Map<string, PackImageReader>>(new Map());
 | 
						|
    const [deleteImages, setDeleteImages] = useState<Set<string>>(new Set());
 | 
						|
 | 
						|
    const hasImageWithShortcode = useCallback(
 | 
						|
      (shortcode: string): boolean => {
 | 
						|
        const hasInPack = imagePack.images.collection.has(shortcode);
 | 
						|
        if (hasInPack) return true;
 | 
						|
        const hasInUploaded =
 | 
						|
          uploadedImages.find((img) => img.shortcode === shortcode) !== undefined;
 | 
						|
        if (hasInUploaded) return true;
 | 
						|
        const hasInSaved =
 | 
						|
          Array.from(savedImages).find(([, img]) => img.shortcode === shortcode) !== undefined;
 | 
						|
        return hasInSaved;
 | 
						|
      },
 | 
						|
      [imagePack, savedImages, uploadedImages]
 | 
						|
    );
 | 
						|
 | 
						|
    const pickFiles = useFilePicker(
 | 
						|
      useCallback(
 | 
						|
        (pickedFiles: File[]) => {
 | 
						|
          const uniqueFiles = pickedFiles.map((file) => {
 | 
						|
            const fileName = replaceSpaceWithDash(file.name);
 | 
						|
            if (hasImageWithShortcode(fileName)) {
 | 
						|
              const uniqueName = suffixRename(fileName, hasImageWithShortcode);
 | 
						|
              return renameFile(file, uniqueName);
 | 
						|
            }
 | 
						|
            return fileName !== file.name ? renameFile(file, fileName) : file;
 | 
						|
          });
 | 
						|
 | 
						|
          setFiles((f) => [...f, ...uniqueFiles]);
 | 
						|
        },
 | 
						|
        [hasImageWithShortcode]
 | 
						|
      ),
 | 
						|
      true
 | 
						|
    );
 | 
						|
 | 
						|
    const handleMetaSave = useCallback(
 | 
						|
      (editedMeta: PackMetaReader) => {
 | 
						|
        setMetaEditing(false);
 | 
						|
        setSavedMeta(
 | 
						|
          (m) =>
 | 
						|
            new PackMetaReader({
 | 
						|
              ...imagePack.meta.content,
 | 
						|
              ...m?.content,
 | 
						|
              ...editedMeta.content,
 | 
						|
            })
 | 
						|
        );
 | 
						|
      },
 | 
						|
      [imagePack.meta]
 | 
						|
    );
 | 
						|
 | 
						|
    const handleMetaCancel = () => setMetaEditing(false);
 | 
						|
 | 
						|
    const handlePackUsageChange = useCallback(
 | 
						|
      (usg: ImageUsage[]) => {
 | 
						|
        setSavedMeta(
 | 
						|
          (m) =>
 | 
						|
            new PackMetaReader({
 | 
						|
              ...imagePack.meta.content,
 | 
						|
              ...m?.content,
 | 
						|
              usage: usg,
 | 
						|
            })
 | 
						|
        );
 | 
						|
      },
 | 
						|
      [imagePack.meta]
 | 
						|
    );
 | 
						|
 | 
						|
    const handleUploadRemove = useCallback((file: TUploadContent) => {
 | 
						|
      setFiles((fs) => fs.filter((f) => f !== file));
 | 
						|
    }, []);
 | 
						|
 | 
						|
    const handleUploadComplete = useCallback(
 | 
						|
      async (data: UploadSuccess) => {
 | 
						|
        const imgEl = await loadImageElement(getImageFileUrl(data.file));
 | 
						|
        const packImage: PackImage = {
 | 
						|
          url: data.mxc,
 | 
						|
          info: getImageInfo(imgEl, data.file),
 | 
						|
        };
 | 
						|
        const image = PackImageReader.fromPackImage(
 | 
						|
          getFileNameWithoutExt(data.file.name),
 | 
						|
          packImage
 | 
						|
        );
 | 
						|
        if (!image) return;
 | 
						|
        handleUploadRemove(data.file);
 | 
						|
        setUploadedImages((imgs) => [image, ...imgs]);
 | 
						|
      },
 | 
						|
      [handleUploadRemove]
 | 
						|
    );
 | 
						|
 | 
						|
    const handleImageEdit = (shortcode: string) => {
 | 
						|
      setImagesEditing((shortcodes) => {
 | 
						|
        const shortcodeSet = new Set(shortcodes);
 | 
						|
        shortcodeSet.add(shortcode);
 | 
						|
        return shortcodeSet;
 | 
						|
      });
 | 
						|
    };
 | 
						|
    const handleDeleteToggle = (shortcode: string) => {
 | 
						|
      setDeleteImages((shortcodes) => {
 | 
						|
        const shortcodeSet = new Set(shortcodes);
 | 
						|
        if (shortcodeSet.has(shortcode)) shortcodeSet.delete(shortcode);
 | 
						|
        else shortcodeSet.add(shortcode);
 | 
						|
        return shortcodeSet;
 | 
						|
      });
 | 
						|
    };
 | 
						|
 | 
						|
    const handleImageEditCancel = (shortcode: string) => {
 | 
						|
      setImagesEditing((shortcodes) => {
 | 
						|
        const shortcodeSet = new Set(shortcodes);
 | 
						|
        shortcodeSet.delete(shortcode);
 | 
						|
        return shortcodeSet;
 | 
						|
      });
 | 
						|
    };
 | 
						|
 | 
						|
    const handleImageEditSave = (shortcode: string, image: PackImageReader) => {
 | 
						|
      handleImageEditCancel(shortcode);
 | 
						|
 | 
						|
      const saveImage =
 | 
						|
        shortcode !== image.shortcode && hasImageWithShortcode(image.shortcode)
 | 
						|
          ? new PackImageReader(
 | 
						|
              suffixRename(image.shortcode, hasImageWithShortcode),
 | 
						|
              image.url,
 | 
						|
              image.content
 | 
						|
            )
 | 
						|
          : image;
 | 
						|
 | 
						|
      setSavedImages((sImgs) => {
 | 
						|
        const imgs = new Map(sImgs);
 | 
						|
        imgs.set(shortcode, saveImage);
 | 
						|
        return imgs;
 | 
						|
      });
 | 
						|
    };
 | 
						|
 | 
						|
    const handleResetSavedChanges = () => {
 | 
						|
      setSavedMeta(undefined);
 | 
						|
      setFiles([]);
 | 
						|
      setUploadedImages([]);
 | 
						|
      setSavedImages(new Map());
 | 
						|
      setDeleteImages(new Set());
 | 
						|
    };
 | 
						|
 | 
						|
    const [applyState, applyChanges] = useAsyncCallback(
 | 
						|
      useCallback(async () => {
 | 
						|
        const pack: PackContent = {
 | 
						|
          pack: savedMeta?.content ?? imagePack.meta.content,
 | 
						|
          images: {},
 | 
						|
        };
 | 
						|
        const pushImage = (img: PackImageReader) => {
 | 
						|
          if (deleteImages.has(img.shortcode)) return;
 | 
						|
          if (!pack.images) return;
 | 
						|
          const imgToPush = savedImages.get(img.shortcode) ?? img;
 | 
						|
          pack.images[imgToPush.shortcode] = imgToPush.content;
 | 
						|
        };
 | 
						|
        uploadedImages.forEach((img) => pushImage(img));
 | 
						|
        images.forEach((img) => pushImage(img));
 | 
						|
 | 
						|
        return onUpdate?.(pack);
 | 
						|
      }, [imagePack, images, savedMeta, uploadedImages, savedImages, deleteImages, onUpdate])
 | 
						|
    );
 | 
						|
 | 
						|
    useEffect(() => {
 | 
						|
      if (applyState.status === AsyncStatus.Success) {
 | 
						|
        handleResetSavedChanges();
 | 
						|
      }
 | 
						|
    }, [applyState]);
 | 
						|
 | 
						|
    const savedChanges =
 | 
						|
      (savedMeta && !packMetaEqual(imagePack.meta, savedMeta)) ||
 | 
						|
      uploadedImages.length > 0 ||
 | 
						|
      savedImages.size > 0 ||
 | 
						|
      deleteImages.size > 0;
 | 
						|
    const canApplyChanges = !metaEditing && imagesEditing.size === 0 && files.length === 0;
 | 
						|
    const applying = applyState.status === AsyncStatus.Loading;
 | 
						|
 | 
						|
    const renderImage = (image: PackImageReader) => (
 | 
						|
      <SequenceCard
 | 
						|
        key={image.shortcode}
 | 
						|
        style={{ padding: config.space.S300 }}
 | 
						|
        variant={deleteImages.has(image.shortcode) ? 'Critical' : 'SurfaceVariant'}
 | 
						|
        direction="Column"
 | 
						|
        gap="400"
 | 
						|
      >
 | 
						|
        {imagesEditing.has(image.shortcode) ? (
 | 
						|
          <ImageTileEdit
 | 
						|
            defaultShortcode={image.shortcode}
 | 
						|
            image={savedImages.get(image.shortcode) ?? image}
 | 
						|
            packUsage={currentMeta.usage}
 | 
						|
            useAuthentication={useAuthentication}
 | 
						|
            onCancel={handleImageEditCancel}
 | 
						|
            onSave={handleImageEditSave}
 | 
						|
          />
 | 
						|
        ) : (
 | 
						|
          <ImageTile
 | 
						|
            defaultShortcode={image.shortcode}
 | 
						|
            image={savedImages.get(image.shortcode) ?? image}
 | 
						|
            packUsage={currentMeta.usage}
 | 
						|
            useAuthentication={useAuthentication}
 | 
						|
            canEdit={canEdit}
 | 
						|
            onEdit={handleImageEdit}
 | 
						|
            deleted={deleteImages.has(image.shortcode)}
 | 
						|
            onDeleteToggle={handleDeleteToggle}
 | 
						|
          />
 | 
						|
        )}
 | 
						|
      </SequenceCard>
 | 
						|
    );
 | 
						|
 | 
						|
    return (
 | 
						|
      <Box grow="Yes" direction="Column" gap="700" {...props} ref={ref}>
 | 
						|
        {savedChanges && (
 | 
						|
          <Menu className={css.UnsavedMenu} variant="Success">
 | 
						|
            <Box alignItems="Center" gap="400">
 | 
						|
              <Box grow="Yes" direction="Column">
 | 
						|
                {applyState.status === AsyncStatus.Error ? (
 | 
						|
                  <Text size="T200">
 | 
						|
                    <b>Failed to apply changes! Please try again.</b>
 | 
						|
                  </Text>
 | 
						|
                ) : (
 | 
						|
                  <Text size="T200">
 | 
						|
                    <b>Changes saved! Apply when ready.</b>
 | 
						|
                  </Text>
 | 
						|
                )}
 | 
						|
              </Box>
 | 
						|
              <Box shrink="No" gap="200">
 | 
						|
                <Button
 | 
						|
                  size="300"
 | 
						|
                  variant="Success"
 | 
						|
                  fill="None"
 | 
						|
                  radii="300"
 | 
						|
                  disabled={!canApplyChanges || applying}
 | 
						|
                  onClick={handleResetSavedChanges}
 | 
						|
                >
 | 
						|
                  <Text size="B300">Reset</Text>
 | 
						|
                </Button>
 | 
						|
                <Button
 | 
						|
                  size="300"
 | 
						|
                  variant="Success"
 | 
						|
                  radii="300"
 | 
						|
                  disabled={!canApplyChanges || applying}
 | 
						|
                  before={applying && <Spinner variant="Success" fill="Solid" size="100" />}
 | 
						|
                  onClick={applyChanges}
 | 
						|
                >
 | 
						|
                  <Text size="B300">Apply Changes</Text>
 | 
						|
                </Button>
 | 
						|
              </Box>
 | 
						|
            </Box>
 | 
						|
          </Menu>
 | 
						|
        )}
 | 
						|
        <Box direction="Column" gap="100">
 | 
						|
          <Text size="L400">Pack</Text>
 | 
						|
          <SequenceCard
 | 
						|
            style={{ padding: config.space.S300 }}
 | 
						|
            variant="SurfaceVariant"
 | 
						|
            direction="Column"
 | 
						|
            gap="400"
 | 
						|
          >
 | 
						|
            {metaEditing ? (
 | 
						|
              <ImagePackProfileEdit
 | 
						|
                meta={currentMeta}
 | 
						|
                onCancel={handleMetaCancel}
 | 
						|
                onSave={handleMetaSave}
 | 
						|
              />
 | 
						|
            ) : (
 | 
						|
              <ImagePackProfile
 | 
						|
                meta={currentMeta}
 | 
						|
                canEdit={canEdit}
 | 
						|
                onEdit={() => setMetaEditing(true)}
 | 
						|
              />
 | 
						|
            )}
 | 
						|
          </SequenceCard>
 | 
						|
          <SequenceCard
 | 
						|
            style={{ padding: config.space.S300 }}
 | 
						|
            variant="SurfaceVariant"
 | 
						|
            direction="Column"
 | 
						|
            gap="400"
 | 
						|
          >
 | 
						|
            <SettingTile
 | 
						|
              title="Images Usage"
 | 
						|
              description="Select how the images are being used: as emojis, as stickers, or as both."
 | 
						|
              after={
 | 
						|
                <UsageSwitcher
 | 
						|
                  usage={currentMeta.usage}
 | 
						|
                  canEdit={canEdit}
 | 
						|
                  onChange={handlePackUsageChange}
 | 
						|
                />
 | 
						|
              }
 | 
						|
            />
 | 
						|
          </SequenceCard>
 | 
						|
        </Box>
 | 
						|
        {images.length === 0 && !canEdit ? null : (
 | 
						|
          <Box direction="Column" gap="100">
 | 
						|
            <Text size="L400">Images</Text>
 | 
						|
            {canEdit && (
 | 
						|
              <SequenceCard
 | 
						|
                style={{ padding: config.space.S300 }}
 | 
						|
                variant="SurfaceVariant"
 | 
						|
                direction="Column"
 | 
						|
                gap="400"
 | 
						|
              >
 | 
						|
                <SettingTile
 | 
						|
                  title="Upload Images"
 | 
						|
                  description="Select images from your storage to upload them in pack."
 | 
						|
                  after={
 | 
						|
                    <Button
 | 
						|
                      variant="Secondary"
 | 
						|
                      fill="Soft"
 | 
						|
                      size="300"
 | 
						|
                      radii="300"
 | 
						|
                      type="button"
 | 
						|
                      outlined
 | 
						|
                      onClick={() => pickFiles('image/*')}
 | 
						|
                    >
 | 
						|
                      <Text size="B300">Select</Text>
 | 
						|
                    </Button>
 | 
						|
                  }
 | 
						|
                />
 | 
						|
              </SequenceCard>
 | 
						|
            )}
 | 
						|
            {files.map((file) => (
 | 
						|
              <SequenceCard
 | 
						|
                key={file.name}
 | 
						|
                style={{ padding: config.space.S300 }}
 | 
						|
                variant="SurfaceVariant"
 | 
						|
                direction="Column"
 | 
						|
                gap="400"
 | 
						|
              >
 | 
						|
                <ImageTileUpload file={file}>
 | 
						|
                  {(uploadAtom) => (
 | 
						|
                    <CompactUploadCardRenderer
 | 
						|
                      uploadAtom={uploadAtom}
 | 
						|
                      onRemove={handleUploadRemove}
 | 
						|
                      onComplete={handleUploadComplete}
 | 
						|
                    />
 | 
						|
                  )}
 | 
						|
                </ImageTileUpload>
 | 
						|
              </SequenceCard>
 | 
						|
            ))}
 | 
						|
            {uploadedImages.map(renderImage)}
 | 
						|
            {images.map(renderImage)}
 | 
						|
          </Box>
 | 
						|
        )}
 | 
						|
      </Box>
 | 
						|
    );
 | 
						|
  }
 | 
						|
);
 |