add option to upload pack images and apply changes

This commit is contained in:
Ajay Bura 2025-01-15 14:26:50 +05:30
parent 632e54d97e
commit 7b41fde237
12 changed files with 362 additions and 78 deletions

View file

@ -1,38 +1,84 @@
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { as, Box, Text, config, Button, Menu } from 'folds'; import { as, Box, Text, config, Button, Menu, Spinner } from 'folds';
import { import {
ImagePack, ImagePack,
ImageUsage, ImageUsage,
PackContent,
PackImage,
PackImageReader, PackImageReader,
packMetaEqual, packMetaEqual,
PackMetaReader, PackMetaReader,
} from '../../plugins/custom-emoji'; } from '../../plugins/custom-emoji';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { SequenceCard } from '../sequence-card'; import { SequenceCard } from '../sequence-card';
import { ImageTile, ImageTileEdit } from './ImageTile'; import { ImageTile, ImageTileEdit, ImageTileUpload } from './ImageTile';
import { SettingTile } from '../setting-tile'; import { SettingTile } from '../setting-tile';
import { UsageSwitcher } from './UsageSwitcher'; import { UsageSwitcher } from './UsageSwitcher';
import { ImagePackProfile, ImagePackProfileEdit } from './PackMeta'; import { ImagePackProfile, ImagePackProfileEdit } from './PackMeta';
import * as css from './style.css'; 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 = { export type ImagePackContentProps = {
imagePack: ImagePack; imagePack: ImagePack;
canEdit?: boolean; canEdit?: boolean;
onUpdate?: (packContent: PackContent) => Promise<void>;
}; };
export const ImagePackContent = as<'div', ImagePackContentProps>( export const ImagePackContent = as<'div', ImagePackContentProps>(
({ imagePack, canEdit, ...props }, ref) => { ({ imagePack, canEdit, onUpdate, ...props }, ref) => {
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
const images = useMemo(() => Array.from(imagePack.images.collection.values()), [imagePack]);
const [metaEditing, setMetaEditing] = useState(false); const [metaEditing, setMetaEditing] = useState(false);
const [savedMeta, setSavedMeta] = useState<PackMetaReader>(); const [savedMeta, setSavedMeta] = useState<PackMetaReader>();
const currentMeta = savedMeta ?? imagePack.meta; 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 [imagesEditing, setImagesEditing] = useState<Set<string>>(new Set());
const [savedImages, setSavedImages] = useState<Map<string, PackImageReader>>(new Map()); const [savedImages, setSavedImages] = useState<Map<string, PackImageReader>>(new Map());
const [deleteImages, setDeleteImages] = useState<Set<string>>(new Set()); 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( const handleMetaSave = useCallback(
(editedMeta: PackMetaReader) => { (editedMeta: PackMetaReader) => {
setMetaEditing(false); setMetaEditing(false);
@ -64,6 +110,28 @@ export const ImagePackContent = as<'div', ImagePackContentProps>(
[imagePack.meta] [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) => { const handleImageEdit = (shortcode: string) => {
setImagesEditing((shortcodes) => { setImagesEditing((shortcodes) => {
const shortcodeSet = new Set(shortcodes); const shortcodeSet = new Set(shortcodes);
@ -90,27 +158,95 @@ export const ImagePackContent = as<'div', ImagePackContentProps>(
const handleImageEditSave = (shortcode: string, image: PackImageReader) => { const handleImageEditSave = (shortcode: string, image: PackImageReader) => {
handleImageEditCancel(shortcode); handleImageEditCancel(shortcode);
const saveImage =
shortcode !== image.shortcode && hasImageWithShortcode(image.shortcode)
? new PackImageReader(
suffixRename(image.shortcode, hasImageWithShortcode),
image.url,
image.content
)
: image;
setSavedImages((sImgs) => { setSavedImages((sImgs) => {
const imgs = new Map(sImgs); const imgs = new Map(sImgs);
imgs.set(shortcode, image); imgs.set(shortcode, saveImage);
return imgs; return imgs;
}); });
}; };
const handleResetSavedChanges = () => { const handleResetSavedChanges = () => {
setSavedMeta(undefined); setSavedMeta(undefined);
setFiles([]);
setUploadedImages([]);
setSavedImages(new Map()); setSavedImages(new Map());
setDeleteImages(new Set()); setDeleteImages(new Set());
}; };
const handleApplySavedChanges = () => {
setSavedMeta(undefined); 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 = const savedChanges =
(savedMeta && !packMetaEqual(imagePack.meta, savedMeta)) || (savedMeta && !packMetaEqual(imagePack.meta, savedMeta)) ||
uploadedImages.length > 0 ||
savedImages.size > 0 || savedImages.size > 0 ||
deleteImages.size > 0; deleteImages.size > 0;
const canApplyChanges = !metaEditing && imagesEditing.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 ( return (
<Box grow="Yes" direction="Column" gap="700" {...props} ref={ref}> <Box grow="Yes" direction="Column" gap="700" {...props} ref={ref}>
@ -118,9 +254,15 @@ export const ImagePackContent = as<'div', ImagePackContentProps>(
<Menu className={css.UnsavedMenu} variant="Success"> <Menu className={css.UnsavedMenu} variant="Success">
<Box alignItems="Center" gap="400"> <Box alignItems="Center" gap="400">
<Box grow="Yes" direction="Column"> <Box grow="Yes" direction="Column">
<Text size="T200"> {applyState.status === AsyncStatus.Error ? (
<b>Changes saved! Apply when ready.</b> <Text size="T200">
</Text> <b>Failed to apply changes! Please try again.</b>
</Text>
) : (
<Text size="T200">
<b>Changes saved! Apply when ready.</b>
</Text>
)}
</Box> </Box>
<Box shrink="No" gap="200"> <Box shrink="No" gap="200">
<Button <Button
@ -128,7 +270,7 @@ export const ImagePackContent = as<'div', ImagePackContentProps>(
variant="Success" variant="Success"
fill="None" fill="None"
radii="300" radii="300"
disabled={!canApplyChanges} disabled={!canApplyChanges || applying}
onClick={handleResetSavedChanges} onClick={handleResetSavedChanges}
> >
<Text size="B300">Reset</Text> <Text size="B300">Reset</Text>
@ -137,8 +279,9 @@ export const ImagePackContent = as<'div', ImagePackContentProps>(
size="300" size="300"
variant="Success" variant="Success"
radii="300" radii="300"
disabled={!canApplyChanges} disabled={!canApplyChanges || applying}
onClick={handleApplySavedChanges} before={applying && <Spinner variant="Success" fill="Soft" size="100" />}
onClick={applyChanges}
> >
<Text size="B300">Apply Changes</Text> <Text size="B300">Apply Changes</Text>
</Button> </Button>
@ -187,44 +330,54 @@ export const ImagePackContent = as<'div', ImagePackContentProps>(
/> />
</SequenceCard> </SequenceCard>
</Box> </Box>
{images.length > 0 && ( <Box direction="Column" gap="100">
<Box direction="Column" gap="100"> <Text size="L400">Images</Text>
<Text size="L400">Images</Text> <SequenceCard
<Box direction="Column" gap="100"> style={{ padding: config.space.S300 }}
{images.map((image) => ( variant="SurfaceVariant"
<SequenceCard direction="Column"
key={image.shortcode} gap="400"
style={{ padding: config.space.S300 }} >
variant={deleteImages.has(image.shortcode) ? 'Critical' : 'SurfaceVariant'} <SettingTile
direction="Column" title="Upload Images"
gap="400" 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/*')}
> >
{imagesEditing.has(image.shortcode) ? ( <Text size="B300">Select</Text>
<ImageTileEdit </Button>
defaultShortcode={image.shortcode} }
image={savedImages.get(image.shortcode) ?? image} />
packUsage={currentMeta.usage} </SequenceCard>
useAuthentication={useAuthentication} {files.map((file) => (
onCancel={handleImageEditCancel} <SequenceCard
onSave={handleImageEditSave} key={file.name}
/> style={{ padding: config.space.S300 }}
) : ( variant="SurfaceVariant"
<ImageTile direction="Column"
defaultShortcode={image.shortcode} gap="400"
image={savedImages.get(image.shortcode) ?? image} >
packUsage={currentMeta.usage} <ImageTileUpload file={file}>
useAuthentication={useAuthentication} {(uploadAtom) => (
canEdit={canEdit} <CompactUploadCardRenderer
onEdit={handleImageEdit} uploadAtom={uploadAtom}
deleted={deleteImages.has(image.shortcode)} onRemove={handleUploadRemove}
onDeleteToggle={handleDeleteToggle} onComplete={handleUploadComplete}
/> />
)} )}
</SequenceCard> </ImageTileUpload>
))} </SequenceCard>
</Box> ))}
</Box> {uploadedImages.map(renderImage)}
)} {images.map(renderImage)}
</Box>
</Box> </Box>
); );
} }

View file

@ -1,19 +1,18 @@
import React from 'react'; import React from 'react';
import { Box, IconButton, Text, Icon, Icons, Scroll, Chip } from 'folds'; import { Box, IconButton, Text, Icon, Icons, Scroll, Chip } from 'folds';
import { ImagePack } from '../../plugins/custom-emoji'; import { PackAddress } from '../../plugins/custom-emoji';
import { Page, PageHeader, PageContent } from '../page'; import { Page, PageHeader, PageContent } from '../page';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { RoomImagePack } from './RoomImagePack'; import { RoomImagePack } from './RoomImagePack';
import { UserImagePack } from './UserImagePack'; import { UserImagePack } from './UserImagePack';
type ImagePackViewProps = { type ImagePackViewProps = {
imagePack: ImagePack; address: PackAddress | undefined;
requestClose: () => void; requestClose: () => void;
}; };
export function ImagePackView({ imagePack, requestClose }: ImagePackViewProps) { export function ImagePackView({ address, requestClose }: ImagePackViewProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { roomId } = imagePack.address ?? {}; const room = address && mx.getRoom(address.roomId);
const room = mx.getRoom(roomId);
return ( return (
<Page> <Page>
@ -39,10 +38,10 @@ export function ImagePackView({ imagePack, requestClose }: ImagePackViewProps) {
<Box grow="Yes"> <Box grow="Yes">
<Scroll hideTrack visibility="Hover"> <Scroll hideTrack visibility="Hover">
<PageContent> <PageContent>
{room ? ( {room && address ? (
<RoomImagePack room={room} imagePack={imagePack} /> <RoomImagePack room={room} stateKey={address.stateKey} />
) : ( ) : (
<UserImagePack imagePack={imagePack} /> <UserImagePack />
)} )}
</PageContent> </PageContent>
</Scroll> </Scroll>

View file

@ -1,4 +1,4 @@
import React, { FormEventHandler, useState } from 'react'; import React, { FormEventHandler, ReactNode, useMemo, useState } from 'react';
import { Badge, Box, Button, Chip, Icon, Icons, Input, Text } from 'folds'; import { Badge, Box, Button, Chip, Icon, Icons, Input, Text } from 'folds';
import { UsageSwitcher, useUsageStr } from './UsageSwitcher'; import { UsageSwitcher, useUsageStr } from './UsageSwitcher';
import { mxcUrlToHttp } from '../../utils/matrix'; import { mxcUrlToHttp } from '../../utils/matrix';
@ -6,6 +6,9 @@ import * as css from './style.css';
import { ImageUsage, imageUsageEqual, PackImageReader } from '../../plugins/custom-emoji'; import { ImageUsage, imageUsageEqual, PackImageReader } from '../../plugins/custom-emoji';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { SettingTile } from '../setting-tile'; import { SettingTile } from '../setting-tile';
import { useObjectURL } from '../../hooks/useObjectURL';
import { createUploadAtom, TUploadAtom } from '../../state/upload';
import { replaceSpaceWithDash } from '../../utils/common';
type ImageTileProps = { type ImageTileProps = {
defaultShortcode: string; defaultShortcode: string;
@ -85,6 +88,21 @@ export function ImageTile({
); );
} }
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 = { type ImageTileEditProps = {
defaultShortcode: string; defaultShortcode: string;
useAuthentication: boolean; useAuthentication: boolean;
@ -114,12 +132,21 @@ export function ImageTileEdit({
const bodyInput = target?.bodyInput as HTMLTextAreaElement | undefined; const bodyInput = target?.bodyInput as HTMLTextAreaElement | undefined;
if (!shortcodeInput || !bodyInput) return; if (!shortcodeInput || !bodyInput) return;
const shortcode = shortcodeInput.value.trim(); const shortcode = replaceSpaceWithDash(shortcodeInput.value.trim());
const body = bodyInput.value.trim(); const body = bodyInput.value.trim() || undefined;
const usage = unsavedUsage; const usage = unsavedUsage;
if (!shortcode) return; if (!shortcode) return;
if (
shortcode === image.shortcode &&
body === image.body &&
imageUsageEqual(usage, defaultUsage)
) {
onCancel(defaultShortcode);
return;
}
const imageReader = new PackImageReader(shortcode, image.url, { const imageReader = new PackImageReader(shortcode, image.url, {
info: image.info, info: image.info,
body, body,

View file

@ -1,17 +1,19 @@
import React from 'react'; import React, { useCallback, useMemo } from 'react';
import { Room } from 'matrix-js-sdk'; import { Room } from 'matrix-js-sdk';
import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels'; import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { ImagePackContent } from './ImagePackContent'; import { ImagePackContent } from './ImagePackContent';
import { ImagePack } from '../../plugins/custom-emoji'; import { ImagePack, PackContent } from '../../plugins/custom-emoji';
import { StateEvent } from '../../../types/matrix/room'; import { StateEvent } from '../../../types/matrix/room';
import { useRoomImagePack } from '../../hooks/useImagePacks';
import { randomStr } from '../../utils/common';
type RoomImagePackProps = { type RoomImagePackProps = {
room: Room; room: Room;
imagePack: ImagePack; stateKey: string;
}; };
export function RoomImagePack({ room, imagePack }: RoomImagePackProps) { export function RoomImagePack({ room, stateKey }: RoomImagePackProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const userId = mx.getUserId()!; const userId = mx.getUserId()!;
const powerLevels = usePowerLevels(room); const powerLevels = usePowerLevels(room);
@ -19,5 +21,35 @@ export function RoomImagePack({ room, imagePack }: RoomImagePackProps) {
const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels); const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels);
const canEditImagePack = canSendStateEvent(StateEvent.PoniesRoomEmotes, getPowerLevel(userId)); const canEditImagePack = canSendStateEvent(StateEvent.PoniesRoomEmotes, getPowerLevel(userId));
return <ImagePackContent imagePack={imagePack} canEdit={canEditImagePack} />; const fallbackPack = useMemo(() => {
const fakePackId = randomStr(4);
return new ImagePack(
fakePackId,
{},
{
roomId: room.roomId,
stateKey,
}
);
}, [room.roomId, stateKey]);
const imagePack = useRoomImagePack(room, stateKey) ?? fallbackPack;
const handleUpdate = useCallback(
async (packContent: PackContent) => {
const { address } = imagePack;
if (!address) return;
await mx.sendStateEvent(
address.roomId,
'StateEvent.PoniesRoomEmotes',
packContent,
address.stateKey
);
},
[mx, imagePack]
);
return (
<ImagePackContent imagePack={imagePack} canEdit={canEditImagePack} onUpdate={handleUpdate} />
);
} }

View file

@ -1,11 +1,22 @@
import React from 'react'; import React, { useCallback, useMemo } from 'react';
import { ImagePackContent } from './ImagePackContent'; import { ImagePackContent } from './ImagePackContent';
import { ImagePack } from '../../plugins/custom-emoji'; import { ImagePack, PackContent } from '../../plugins/custom-emoji';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { AccountDataEvent } from '../../../types/matrix/accountData';
import { useUserImagePack } from '../../hooks/useImagePacks';
type UserImagePackProps = { export function UserImagePack() {
imagePack: ImagePack; const mx = useMatrixClient();
};
export function UserImagePack({ imagePack }: UserImagePackProps) { const defaultPack = useMemo(() => new ImagePack(mx.getUserId() ?? '', {}, undefined), [mx]);
return <ImagePackContent imagePack={imagePack} canEdit />; const imagePack = useUserImagePack();
const handleUpdate = useCallback(
async (packContent: PackContent) => {
await mx.setAccountData(AccountDataEvent.PoniesUserEmotes, packContent);
},
[mx]
);
return <ImagePackContent imagePack={imagePack ?? defaultPack} canEdit onUpdate={handleUpdate} />;
} }

View file

@ -17,7 +17,7 @@ export function EmojisStickers({ requestClose }: EmojisStickersProps) {
}; };
if (imagePack) { if (imagePack) {
return <ImagePackView imagePack={imagePack} requestClose={handleImagePackViewClose} />; return <ImagePackView address={imagePack.address} requestClose={handleImagePackViewClose} />;
} }
return ( return (

View file

@ -4,6 +4,7 @@ import { AccountDataEvent } from '../../types/matrix/accountData';
import { StateEvent } from '../../types/matrix/room'; import { StateEvent } from '../../types/matrix/room';
import { import {
getGlobalImagePacks, getGlobalImagePacks,
getRoomImagePack,
getRoomImagePacks, getRoomImagePacks,
getUserImagePack, getUserImagePack,
ImagePack, ImagePack,
@ -72,6 +73,29 @@ export const useGlobalImagePacks = (): ImagePack[] => {
return globalPacks; return globalPacks;
}; };
export const useRoomImagePack = (room: Room, stateKey: string): ImagePack | undefined => {
const mx = useMatrixClient();
const [roomPack, setRoomPack] = useState(() => getRoomImagePack(room, stateKey));
useStateEventCallback(
mx,
useCallback(
(mEvent) => {
if (
mEvent.getRoomId() === room.roomId &&
mEvent.getType() === StateEvent.PoniesRoomEmotes &&
mEvent.getStateKey() === stateKey
) {
setRoomPack(getRoomImagePack(room, stateKey));
}
},
[room, stateKey]
)
);
return roomPack;
};
export const useRoomImagePacks = (room: Room): ImagePack[] => { export const useRoomImagePacks = (room: Room): ImagePack[] => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const [roomPacks, setRoomPacks] = useState(() => getRoomImagePacks(room)); const [roomPacks, setRoomPacks] = useState(() => getRoomImagePacks(room));

View file

@ -40,4 +40,13 @@ export class PackImageReader {
return knownUsage.length > 0 ? knownUsage : undefined; return knownUsage.length > 0 ? knownUsage : undefined;
} }
get content(): PackImage {
return {
url: this.url,
body: this.image.body,
usage: this.image.usage,
info: this.image.info,
};
}
} }

View file

@ -2,7 +2,7 @@ import { MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk';
import { ImagePack } from './ImagePack'; import { ImagePack } from './ImagePack';
import { EmoteRoomsContent, ImageUsage } from './types'; import { EmoteRoomsContent, ImageUsage } from './types';
import { StateEvent } from '../../../types/matrix/room'; import { StateEvent } from '../../../types/matrix/room';
import { getAccountData, getStateEvents } from '../../utils/room'; import { getAccountData, getStateEvent, getStateEvents } from '../../utils/room';
import { AccountDataEvent } from '../../../types/matrix/accountData'; import { AccountDataEvent } from '../../../types/matrix/accountData';
import { PackMetaReader } from './PackMetaReader'; import { PackMetaReader } from './PackMetaReader';
@ -28,6 +28,14 @@ export function makeImagePacks(packEvents: MatrixEvent[]): ImagePack[] {
}, []); }, []);
} }
export function getRoomImagePack(room: Room, stateKey: string): ImagePack | undefined {
const packEvent = getStateEvent(room, StateEvent.PoniesRoomEmotes, stateKey);
if (!packEvent) return undefined;
const packId = packEvent.getId();
if (!packId) return undefined;
return ImagePack.fromMatrixEvent(packId, packEvent);
}
export function getRoomImagePacks(room: Room): ImagePack[] { export function getRoomImagePacks(room: Room): ImagePack[] {
const packEvents = getStateEvents(room, StateEvent.PoniesRoomEmotes); const packEvents = getStateEvents(room, StateEvent.PoniesRoomEmotes);
return makeImagePacks(packEvents); return makeImagePacks(packEvents);

View file

@ -112,3 +112,16 @@ export const randomStr = (len = 12): string => {
} }
return str; return str;
}; };
export const suffixRename = (name: string, validator: (newName: string) => boolean): string => {
let suffix = 1;
let newName = name;
do {
newName = name + suffix;
suffix += 1;
} while (validator(newName));
return newName;
};
export const replaceSpaceWithDash = (str: string): string => str.replace(/ /g, '-');

View file

@ -75,6 +75,9 @@ export const getDataTransferFiles = (dataTransfer: DataTransfer): File[] | undef
return files; return files;
}; };
export const renameFile = (file: File, name: string): File =>
new File([file], name, { type: file.type });
export const getImageUrlBlob = async (url: string) => { export const getImageUrlBlob = async (url: string) => {
const res = await fetch(url); const res = await fetch(url);
const blob = await res.blob(); const blob = await res.blob();

View file

@ -134,3 +134,8 @@ export const getFileNameExt = (fileName: string): string => {
const extStart = fileName.lastIndexOf('.') + 1; const extStart = fileName.lastIndexOf('.') + 1;
return fileName.slice(extStart); return fileName.slice(extStart);
}; };
export const getFileNameWithoutExt = (fileName: string): string => {
const extStart = fileName.lastIndexOf('.');
if (extStart === 0 || extStart === -1) return fileName;
return fileName.slice(0, extStart);
};