215 lines
6.2 KiB
TypeScript
215 lines
6.2 KiB
TypeScript
|
|
import React, { FormEventHandler, ReactNode, useMemo, useState } from 'react';
|
||
|
|
import { Badge, Box, Button, Chip, Icon, Icons, Input, Text } from 'folds';
|
||
|
|
import { UsageSwitcher, useUsageStr } from './UsageSwitcher';
|
||
|
|
import { mxcUrlToHttp } from '../../utils/matrix';
|
||
|
|
import * as css from './style.css';
|
||
|
|
import { ImageUsage, imageUsageEqual, PackImageReader } from '../../plugins/custom-emoji';
|
||
|
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||
|
|
import { SettingTile } from '../setting-tile';
|
||
|
|
import { useObjectURL } from '../../hooks/useObjectURL';
|
||
|
|
import { createUploadAtom, TUploadAtom } from '../../state/upload';
|
||
|
|
import { replaceSpaceWithDash } from '../../utils/common';
|
||
|
|
|
||
|
|
type ImageTileProps = {
|
||
|
|
defaultShortcode: string;
|
||
|
|
useAuthentication: boolean;
|
||
|
|
packUsage: ImageUsage[];
|
||
|
|
image: PackImageReader;
|
||
|
|
canEdit?: boolean;
|
||
|
|
onEdit?: (defaultShortcode: string, image: PackImageReader) => void;
|
||
|
|
deleted?: boolean;
|
||
|
|
onDeleteToggle?: (defaultShortcode: string) => void;
|
||
|
|
};
|
||
|
|
export function ImageTile({
|
||
|
|
defaultShortcode,
|
||
|
|
image,
|
||
|
|
packUsage,
|
||
|
|
useAuthentication,
|
||
|
|
canEdit,
|
||
|
|
onEdit,
|
||
|
|
onDeleteToggle,
|
||
|
|
deleted,
|
||
|
|
}: ImageTileProps) {
|
||
|
|
const mx = useMatrixClient();
|
||
|
|
const getUsageStr = useUsageStr();
|
||
|
|
|
||
|
|
return (
|
||
|
|
<SettingTile
|
||
|
|
before={
|
||
|
|
<img
|
||
|
|
className={css.ImagePackImage}
|
||
|
|
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? ''}
|
||
|
|
alt={image.shortcode}
|
||
|
|
loading="lazy"
|
||
|
|
/>
|
||
|
|
}
|
||
|
|
title={
|
||
|
|
deleted ? (
|
||
|
|
<span className={css.DeleteImageShortcode}>{image.shortcode}</span>
|
||
|
|
) : (
|
||
|
|
image.shortcode
|
||
|
|
)
|
||
|
|
}
|
||
|
|
description={
|
||
|
|
<Box as="span" gap="200">
|
||
|
|
{image.usage && getUsageStr(image.usage) !== getUsageStr(packUsage) && (
|
||
|
|
<Badge as="span" variant="Secondary" size="400" radii="300" outlined>
|
||
|
|
<Text as="span" size="L400">
|
||
|
|
{getUsageStr(image.usage)}
|
||
|
|
</Text>
|
||
|
|
</Badge>
|
||
|
|
)}
|
||
|
|
{image.body}
|
||
|
|
</Box>
|
||
|
|
}
|
||
|
|
after={
|
||
|
|
canEdit ? (
|
||
|
|
<Box shrink="No" alignItems="Center" gap="200">
|
||
|
|
<Chip
|
||
|
|
variant={deleted ? 'Critical' : 'Secondary'}
|
||
|
|
fill="None"
|
||
|
|
radii="Pill"
|
||
|
|
onClick={() => onDeleteToggle?.(defaultShortcode)}
|
||
|
|
>
|
||
|
|
{deleted ? <Text size="B300">Undo</Text> : <Icon size="50" src={Icons.Delete} />}
|
||
|
|
</Chip>
|
||
|
|
{!deleted && (
|
||
|
|
<Chip
|
||
|
|
variant="Secondary"
|
||
|
|
radii="Pill"
|
||
|
|
onClick={() => onEdit?.(defaultShortcode, image)}
|
||
|
|
>
|
||
|
|
<Text size="B300">Edit</Text>
|
||
|
|
</Chip>
|
||
|
|
)}
|
||
|
|
</Box>
|
||
|
|
) : undefined
|
||
|
|
}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
type ImageTileUploadProps = {
|
||
|
|
file: File;
|
||
|
|
children: (uploadAtom: TUploadAtom) => ReactNode;
|
||
|
|
};
|
||
|
|
export function ImageTileUpload({ file, children }: ImageTileUploadProps) {
|
||
|
|
const url = useObjectURL(file);
|
||
|
|
const uploadAtom = useMemo(() => createUploadAtom(file), [file]);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<SettingTile before={<img className={css.ImagePackImage} src={url} alt={file.name} />}>
|
||
|
|
{children(uploadAtom)}
|
||
|
|
</SettingTile>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
type ImageTileEditProps = {
|
||
|
|
defaultShortcode: string;
|
||
|
|
useAuthentication: boolean;
|
||
|
|
packUsage: ImageUsage[];
|
||
|
|
image: PackImageReader;
|
||
|
|
onCancel: (shortcode: string) => void;
|
||
|
|
onSave: (shortcode: string, image: PackImageReader) => void;
|
||
|
|
};
|
||
|
|
export function ImageTileEdit({
|
||
|
|
defaultShortcode,
|
||
|
|
useAuthentication,
|
||
|
|
packUsage,
|
||
|
|
image,
|
||
|
|
onCancel,
|
||
|
|
onSave,
|
||
|
|
}: ImageTileEditProps) {
|
||
|
|
const mx = useMatrixClient();
|
||
|
|
const defaultUsage = image.usage ?? packUsage;
|
||
|
|
|
||
|
|
const [unsavedUsage, setUnsavedUsages] = useState(defaultUsage);
|
||
|
|
|
||
|
|
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
||
|
|
evt.preventDefault();
|
||
|
|
|
||
|
|
const target = evt.target as HTMLFormElement | undefined;
|
||
|
|
const shortcodeInput = target?.shortcodeInput as HTMLInputElement | undefined;
|
||
|
|
const bodyInput = target?.bodyInput as HTMLTextAreaElement | undefined;
|
||
|
|
if (!shortcodeInput || !bodyInput) return;
|
||
|
|
|
||
|
|
const shortcode = replaceSpaceWithDash(shortcodeInput.value.trim());
|
||
|
|
const body = bodyInput.value.trim() || undefined;
|
||
|
|
const usage = unsavedUsage;
|
||
|
|
|
||
|
|
if (!shortcode) return;
|
||
|
|
|
||
|
|
if (
|
||
|
|
shortcode === image.shortcode &&
|
||
|
|
body === image.body &&
|
||
|
|
imageUsageEqual(usage, defaultUsage)
|
||
|
|
) {
|
||
|
|
onCancel(defaultShortcode);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const imageReader = new PackImageReader(shortcode, image.url, {
|
||
|
|
info: image.info,
|
||
|
|
body,
|
||
|
|
usage: imageUsageEqual(usage, packUsage) ? undefined : usage,
|
||
|
|
});
|
||
|
|
|
||
|
|
onSave(defaultShortcode, imageReader);
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<SettingTile
|
||
|
|
before={
|
||
|
|
<img
|
||
|
|
className={css.ImagePackImage}
|
||
|
|
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? ''}
|
||
|
|
alt={image.shortcode}
|
||
|
|
loading="lazy"
|
||
|
|
/>
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<Box as="form" onSubmit={handleSubmit} direction="Column" gap="200">
|
||
|
|
<Box direction="Column" className={css.ImagePackImageInputs}>
|
||
|
|
<Input
|
||
|
|
before={<Text size="L400">Shortcode:</Text>}
|
||
|
|
defaultValue={image.shortcode}
|
||
|
|
name="shortcodeInput"
|
||
|
|
variant="Secondary"
|
||
|
|
size="300"
|
||
|
|
radii="0"
|
||
|
|
required
|
||
|
|
autoFocus
|
||
|
|
/>
|
||
|
|
<Input
|
||
|
|
before={<Text size="L400">Body:</Text>}
|
||
|
|
defaultValue={image.body}
|
||
|
|
name="bodyInput"
|
||
|
|
variant="Secondary"
|
||
|
|
size="300"
|
||
|
|
radii="0"
|
||
|
|
/>
|
||
|
|
</Box>
|
||
|
|
<Box gap="200">
|
||
|
|
<Box shrink="No" direction="Column">
|
||
|
|
<UsageSwitcher usage={unsavedUsage} onChange={setUnsavedUsages} canEdit />
|
||
|
|
</Box>
|
||
|
|
<Box grow="Yes" />
|
||
|
|
<Button type="submit" variant="Success" size="300" radii="300">
|
||
|
|
<Text size="B300">Save</Text>
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
type="reset"
|
||
|
|
variant="Secondary"
|
||
|
|
fill="Soft"
|
||
|
|
size="300"
|
||
|
|
radii="300"
|
||
|
|
onClick={() => onCancel(defaultShortcode)}
|
||
|
|
>
|
||
|
|
<Text size="B300">Cancel</Text>
|
||
|
|
</Button>
|
||
|
|
</Box>
|
||
|
|
</Box>
|
||
|
|
</SettingTile>
|
||
|
|
);
|
||
|
|
}
|