Add/delete/rename images to exisitng packs
This commit is contained in:
parent
7d8c4ce5f1
commit
6ae79e080f
10 changed files with 483 additions and 108 deletions
|
@ -1,15 +1,58 @@
|
||||||
import React, { useState } from 'react';
|
import React, {
|
||||||
|
useState, useMemo, useReducer,
|
||||||
|
} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './ImagePack.scss';
|
import './ImagePack.scss';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
|
import { openReusableDialog } from '../../../client/action/navigation';
|
||||||
|
import { suffixRename } from '../../../util/common';
|
||||||
|
|
||||||
import Button from '../../atoms/button/Button';
|
import Button from '../../atoms/button/Button';
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
|
import Input from '../../atoms/input/Input';
|
||||||
|
import Checkbox from '../../atoms/button/Checkbox';
|
||||||
|
|
||||||
|
import { ImagePack as ImagePackBuilder } from '../../organisms/emoji-board/custom-emoji';
|
||||||
|
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||||
import ImagePackProfile from './ImagePackProfile';
|
import ImagePackProfile from './ImagePackProfile';
|
||||||
import ImagePackItem from './ImagePackItem';
|
import ImagePackItem from './ImagePackItem';
|
||||||
import Checkbox from '../../atoms/button/Checkbox';
|
import ImagePackUpload from './ImagePackUpload';
|
||||||
import { ImagePack as ImagePackBuilder, getUserImagePack } from '../../organisms/emoji-board/custom-emoji';
|
|
||||||
|
const renameImagePackItem = (shortcode) => new Promise((resolve) => {
|
||||||
|
let isCompleted = false;
|
||||||
|
|
||||||
|
openReusableDialog(
|
||||||
|
<Text variant="s1" weight="medium">Rename</Text>,
|
||||||
|
(requestClose) => (
|
||||||
|
<div style={{ padding: 'var(--sp-normal)' }}>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const sc = e.target.shortcode.value;
|
||||||
|
if (sc.trim() === '') return;
|
||||||
|
isCompleted = true;
|
||||||
|
resolve(sc.trim());
|
||||||
|
requestClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={shortcode}
|
||||||
|
name="shortcode"
|
||||||
|
label="Shortcode"
|
||||||
|
autoFocus
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div style={{ height: 'var(--sp-normal)' }} />
|
||||||
|
<Button variant="primary" type="submit">Rename</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
() => {
|
||||||
|
if (!isCompleted) resolve(null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
function getUsage(usage) {
|
function getUsage(usage) {
|
||||||
if (usage.includes('emoticon') && usage.includes('sticker')) return 'both';
|
if (usage.includes('emoticon') && usage.includes('sticker')) return 'both';
|
||||||
|
@ -30,46 +73,159 @@ function isGlobalPack(roomId, stateKey) {
|
||||||
return rooms[roomId]?.[stateKey] !== undefined;
|
return rooms[roomId]?.[stateKey] !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useRoomImagePack(roomId, stateKey) {
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
const room = mx.getRoom(roomId);
|
||||||
|
|
||||||
|
const packEvent = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey);
|
||||||
|
const pack = useMemo(() => (
|
||||||
|
ImagePackBuilder.parsePack(packEvent.getId(), packEvent.getContent(), room)
|
||||||
|
), [room, stateKey]);
|
||||||
|
|
||||||
|
const sendPackContent = (content) => {
|
||||||
|
mx.sendStateEvent(roomId, 'im.ponies.room_emotes', content, stateKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
pack,
|
||||||
|
sendPackContent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function useImagePackHandles(pack, sendPackContent) {
|
||||||
|
const [, forceUpdate] = useReducer((count) => count + 1, 0);
|
||||||
|
|
||||||
|
const getNewKey = (key) => {
|
||||||
|
if (typeof key !== 'string') return undefined;
|
||||||
|
let newKey = key?.replace(/\s/g, '-');
|
||||||
|
if (pack.getImages().get(newKey)) {
|
||||||
|
newKey = suffixRename(
|
||||||
|
newKey,
|
||||||
|
(suffixedKey) => pack.getImages().get(suffixedKey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return newKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditProfile = () => false;
|
||||||
|
const handleUsageChange = (newUsage) => {
|
||||||
|
const usage = [];
|
||||||
|
if (newUsage === 'emoticon' || newUsage === 'both') usage.push('emoticon');
|
||||||
|
if (newUsage === 'sticker' || newUsage === 'both') usage.push('sticker');
|
||||||
|
pack.setUsage(usage);
|
||||||
|
pack.getImages().forEach((img) => pack.setImageUsage(img.shortcode, undefined));
|
||||||
|
|
||||||
|
sendPackContent(pack.getContent());
|
||||||
|
forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRenameItem = async (key) => {
|
||||||
|
const newKey = getNewKey(await renameImagePackItem(key));
|
||||||
|
|
||||||
|
if (!newKey || newKey === key) return;
|
||||||
|
pack.updateImageKey(key, newKey);
|
||||||
|
|
||||||
|
sendPackContent(pack.getContent());
|
||||||
|
forceUpdate();
|
||||||
|
};
|
||||||
|
const handleDeleteItem = async (key) => {
|
||||||
|
const isConfirmed = await confirmDialog(
|
||||||
|
'Delete',
|
||||||
|
`Are you sure that you want to delete "${key}"?`,
|
||||||
|
'Delete',
|
||||||
|
'danger',
|
||||||
|
);
|
||||||
|
if (!isConfirmed) return;
|
||||||
|
pack.removeImage(key);
|
||||||
|
|
||||||
|
sendPackContent(pack.getContent());
|
||||||
|
forceUpdate();
|
||||||
|
};
|
||||||
|
const handleUsageItem = (key, newUsage) => {
|
||||||
|
const usage = [];
|
||||||
|
if (newUsage === 'emoticon' || newUsage === 'both') usage.push('emoticon');
|
||||||
|
if (newUsage === 'sticker' || newUsage === 'both') usage.push('sticker');
|
||||||
|
pack.setImageUsage(key, usage);
|
||||||
|
|
||||||
|
sendPackContent(pack.getContent());
|
||||||
|
forceUpdate();
|
||||||
|
};
|
||||||
|
const handleAddItem = (key, url) => {
|
||||||
|
const newKey = getNewKey(key);
|
||||||
|
if (!newKey || !url) return;
|
||||||
|
|
||||||
|
pack.addImage(newKey, {
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
|
||||||
|
sendPackContent(pack.getContent());
|
||||||
|
forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleEditProfile,
|
||||||
|
handleUsageChange,
|
||||||
|
handleRenameItem,
|
||||||
|
handleDeleteItem,
|
||||||
|
handleUsageItem,
|
||||||
|
handleAddItem,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function ImagePack({ roomId, stateKey }) {
|
function ImagePack({ roomId, stateKey }) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
const [viewMore, setViewMore] = useState(false);
|
const [viewMore, setViewMore] = useState(false);
|
||||||
|
|
||||||
const packEvent = roomId
|
const { pack, sendPackContent } = useRoomImagePack(roomId, stateKey);
|
||||||
? room.currentState.getStateEvents('im.ponies.room_emotes', stateKey)
|
|
||||||
: mx.getAccountData('im.ponies.user_emotes');
|
const {
|
||||||
const pack = roomId
|
handleEditProfile,
|
||||||
? ImagePackBuilder.parsePack(packEvent.getId(), packEvent.getContent(), room)
|
handleUsageChange,
|
||||||
: getUserImagePack(mx);
|
handleRenameItem,
|
||||||
|
handleDeleteItem,
|
||||||
|
handleUsageItem,
|
||||||
|
handleAddItem,
|
||||||
|
} = useImagePackHandles(pack, sendPackContent);
|
||||||
|
|
||||||
|
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
||||||
|
const canChange = room.currentState.hasSufficientPowerLevelFor('state_default', myPowerlevel);
|
||||||
|
|
||||||
|
const images = [...pack.images].slice(0, viewMore ? pack.images.size : 2);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="image-pack">
|
<div className="image-pack">
|
||||||
<ImagePackProfile
|
<ImagePackProfile
|
||||||
avatarUrl={mx.mxcUrlToHttp(pack.avatarUrl ?? pack.getEmojis()[0].mxc)}
|
avatarUrl={mx.mxcUrlToHttp(pack.avatarUrl)}
|
||||||
displayName={pack.displayName}
|
displayName={pack.displayName ?? 'Unknown'}
|
||||||
attribution={pack.attribution}
|
attribution={pack.attribution}
|
||||||
usage={getUsage(pack.usage)}
|
usage={getUsage(pack.usage)}
|
||||||
onUsageChange={(newUsage) => console.log(newUsage)}
|
onUsageChange={canChange ? handleUsageChange : undefined}
|
||||||
onEdit={() => false}
|
onEdit={canChange ? handleEditProfile : undefined}
|
||||||
/>
|
/>
|
||||||
<div>
|
{ canChange && (
|
||||||
<div className="image-pack__header">
|
<ImagePackUpload onUpload={handleAddItem} />
|
||||||
<Text variant="b3">Image</Text>
|
)}
|
||||||
<Text variant="b3">Shortcode</Text>
|
{ images.length === 0 ? null : (
|
||||||
<Text variant="b3">Usage</Text>
|
<div>
|
||||||
|
<div className="image-pack__header">
|
||||||
|
<Text variant="b3">Image</Text>
|
||||||
|
<Text variant="b3">Shortcode</Text>
|
||||||
|
<Text variant="b3">Usage</Text>
|
||||||
|
</div>
|
||||||
|
{images.map(([shortcode, image]) => (
|
||||||
|
<ImagePackItem
|
||||||
|
key={shortcode}
|
||||||
|
url={mx.mxcUrlToHttp(image.mxc)}
|
||||||
|
shortcode={shortcode}
|
||||||
|
usage={getUsage(image.usage)}
|
||||||
|
onUsageChange={canChange ? handleUsageItem : undefined}
|
||||||
|
onDelete={canChange ? handleDeleteItem : undefined}
|
||||||
|
onRename={canChange ? handleRenameItem : undefined}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
{([...pack.images].slice(0, viewMore ? pack.images.size : 2)).map(([shortcode, image]) => (
|
)}
|
||||||
<ImagePackItem
|
|
||||||
key={shortcode}
|
|
||||||
url={mx.mxcUrlToHttp(image.mxc)}
|
|
||||||
shortcode={shortcode}
|
|
||||||
usage={getUsage(image.usage)}
|
|
||||||
onUsageChange={() => false}
|
|
||||||
onDelete={() => false}
|
|
||||||
onRename={() => false}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{pack.images.size > 2 && (
|
{pack.images.size > 2 && (
|
||||||
<div className="image-pack__footer">
|
<div className="image-pack__footer">
|
||||||
<Button onClick={() => setViewMore(!viewMore)}>
|
<Button onClick={() => setViewMore(!viewMore)}>
|
||||||
|
@ -81,27 +237,20 @@ function ImagePack({ roomId, stateKey }) {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{ roomId && (
|
<div className="image-pack__global">
|
||||||
<div className="image-pack__global">
|
<Checkbox variant="positive" isActive={isGlobalPack(roomId, stateKey)} />
|
||||||
<Checkbox variant="positive" isActive={isGlobalPack(roomId, stateKey)} />
|
<div>
|
||||||
<div>
|
<Text variant="b2">Use globally</Text>
|
||||||
<Text variant="b2">Use globally</Text>
|
<Text variant="b3">Add this pack to your account to use in all rooms.</Text>
|
||||||
<Text variant="b3">Add this pack to your account to use in all rooms.</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImagePack.defaultProps = {
|
|
||||||
roomId: null,
|
|
||||||
stateKey: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
ImagePack.propTypes = {
|
ImagePack.propTypes = {
|
||||||
roomId: PropTypes.string,
|
roomId: PropTypes.string.isRequired,
|
||||||
stateKey: PropTypes.string,
|
stateKey: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ImagePack;
|
export default ImagePack;
|
||||||
|
|
|
@ -19,7 +19,9 @@
|
||||||
&__footer {
|
&__footer {
|
||||||
padding: var(--sp-normal);
|
padding: var(--sp-normal);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: var(--sp-tight);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__global {
|
&__global {
|
||||||
padding: var(--sp-normal);
|
padding: var(--sp-normal);
|
||||||
padding-top: var(--sp-tight);
|
padding-top: var(--sp-tight);
|
||||||
|
|
|
@ -27,7 +27,7 @@ function ImagePackItem({
|
||||||
<ImagePackUsageSelector
|
<ImagePackUsageSelector
|
||||||
usage={usage}
|
usage={usage}
|
||||||
onSelect={(newUsage) => {
|
onSelect={(newUsage) => {
|
||||||
onUsageChange(newUsage);
|
onUsageChange(shortcode, newUsage);
|
||||||
closeMenu();
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -43,8 +43,8 @@ function ImagePackItem({
|
||||||
</div>
|
</div>
|
||||||
<div className="image-pack-item__usage">
|
<div className="image-pack-item__usage">
|
||||||
<div className="image-pack-item__btn">
|
<div className="image-pack-item__btn">
|
||||||
{onRename && <IconButton tooltip="Rename" size="extra-small" src={PencilIC} onClick={onRename} />}
|
{onRename && <IconButton tooltip="Rename" size="extra-small" src={PencilIC} onClick={() => onRename(shortcode)} />}
|
||||||
{onDelete && <IconButton tooltip="Delete" size="extra-small" src={BinIC} onClick={onDelete} />}
|
{onDelete && <IconButton tooltip="Delete" size="extra-small" src={BinIC} onClick={() => onDelete(shortcode)} />}
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={onUsageChange ? handleUsageSelect : undefined}>
|
<Button onClick={onUsageChange ? handleUsageSelect : undefined}>
|
||||||
{onUsageChange && <RawIcon src={ChevronBottomIC} size="extra-small" />}
|
{onUsageChange && <RawIcon src={ChevronBottomIC} size="extra-small" />}
|
||||||
|
|
|
@ -35,7 +35,7 @@ function ImagePackProfile({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="image-pack-profile">
|
<div className="image-pack-profile">
|
||||||
<Avatar text={displayName} bgColor="blue" imageSrc={avatarUrl} size="normal" />
|
{avatarUrl && <Avatar text={displayName} bgColor="blue" imageSrc={avatarUrl} size="normal" />}
|
||||||
<div className="image-pack-profile__content">
|
<div className="image-pack-profile__content">
|
||||||
<div>
|
<div>
|
||||||
<Text>{displayName}</Text>
|
<Text>{displayName}</Text>
|
||||||
|
|
72
src/app/molecules/image-pack/ImagePackUpload.jsx
Normal file
72
src/app/molecules/image-pack/ImagePackUpload.jsx
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import React, { useState, useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import './ImagePackUpload.scss';
|
||||||
|
|
||||||
|
import initMatrix from '../../../client/initMatrix';
|
||||||
|
import { scaleDownImage } from '../../../util/common';
|
||||||
|
|
||||||
|
import Text from '../../atoms/text/Text';
|
||||||
|
import Button from '../../atoms/button/Button';
|
||||||
|
import Input from '../../atoms/input/Input';
|
||||||
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
|
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
|
||||||
|
|
||||||
|
function ImagePackUpload({ onUpload }) {
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
const shortcodeRef = useRef(null);
|
||||||
|
const [imgFile, setImgFile] = useState(null);
|
||||||
|
const [progress, setProgress] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
if (!imgFile) return;
|
||||||
|
const { shortcodeInput } = evt.target;
|
||||||
|
const shortcode = shortcodeInput.value.trim();
|
||||||
|
if (shortcode === '') return;
|
||||||
|
|
||||||
|
setProgress(true);
|
||||||
|
const image = await scaleDownImage(imgFile, 512, 512);
|
||||||
|
const url = await mx.uploadContent(image, {
|
||||||
|
onlyContentUri: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
onUpload(shortcode, url);
|
||||||
|
setProgress(false);
|
||||||
|
setImgFile(null);
|
||||||
|
shortcodeRef.current.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (evt) => {
|
||||||
|
const img = evt.target.files[0];
|
||||||
|
if (!img) return;
|
||||||
|
setImgFile(img);
|
||||||
|
};
|
||||||
|
const handleRemove = () => {
|
||||||
|
setImgFile(null);
|
||||||
|
inputRef.current.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="image-pack-upload">
|
||||||
|
<input ref={inputRef} onChange={handleFileChange} style={{ display: 'none' }} type="file" accept=".png, .gif, .webp" required />
|
||||||
|
{
|
||||||
|
imgFile
|
||||||
|
? (
|
||||||
|
<div className="image-pack-upload__file">
|
||||||
|
<IconButton onClick={handleRemove} src={CirclePlusIC} tooltip="Remove file" />
|
||||||
|
<Text>{imgFile.name}</Text>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: <Button onClick={() => inputRef.current.click()}>Import image</Button>
|
||||||
|
}
|
||||||
|
<Input forwardRef={shortcodeRef} name="shortcodeInput" placeholder="shortcode" required />
|
||||||
|
<Button disabled={progress} variant="primary" type="submit">{progress ? 'Uploading...' : 'Upload'}</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ImagePackUpload.propTypes = {
|
||||||
|
onUpload: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImagePackUpload;
|
43
src/app/molecules/image-pack/ImagePackUpload.scss
Normal file
43
src/app/molecules/image-pack/ImagePackUpload.scss
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
@use '../../partials/dir';
|
||||||
|
@use '../../partials/text';
|
||||||
|
|
||||||
|
.image-pack-upload {
|
||||||
|
padding: var(--sp-normal);
|
||||||
|
padding-top: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--sp-tight);
|
||||||
|
|
||||||
|
& > .input-container {
|
||||||
|
flex-grow: 1;
|
||||||
|
input {
|
||||||
|
padding: 9px var(--sp-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__file {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--bg-surface-low);
|
||||||
|
border-radius: var(--bo-radius);
|
||||||
|
box-shadow: var(--bs-surface-border);
|
||||||
|
|
||||||
|
& button {
|
||||||
|
--parent-height: 40px;
|
||||||
|
width: var(--parent-height);
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .ic-raw {
|
||||||
|
background-color: var(--bg-caution);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .text {
|
||||||
|
@extend .cp-txt__ellipsis;
|
||||||
|
@include dir.side(margin, var(--sp-ultra-tight), var(--sp-normal));
|
||||||
|
max-width: 86px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -257,7 +257,7 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||||
{
|
{
|
||||||
availableEmojis.map((pack) => (
|
availableEmojis.map((pack) => (
|
||||||
<EmojiGroup
|
<EmojiGroup
|
||||||
name={pack.displayName}
|
name={pack.displayName ?? 'Unknown'}
|
||||||
key={pack.packIndex}
|
key={pack.packIndex}
|
||||||
groupEmojis={pack.getEmojis()}
|
groupEmojis={pack.getEmojis()}
|
||||||
className="custom-emoji-group"
|
className="custom-emoji-group"
|
||||||
|
@ -297,7 +297,7 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||||
onClick={() => openGroup(recentOffset + pack.packIndex)}
|
onClick={() => openGroup(recentOffset + pack.packIndex)}
|
||||||
src={src}
|
src={src}
|
||||||
key={pack.packIndex}
|
key={pack.packIndex}
|
||||||
tooltip={pack.displayName}
|
tooltip={pack.displayName ?? 'Unknown'}
|
||||||
tooltipPlacement="right"
|
tooltipPlacement="right"
|
||||||
isImage
|
isImage
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -8,20 +8,38 @@ class ImagePack {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pack = packContent.pack ?? {};
|
return new ImagePack(eventId, packContent, room);
|
||||||
|
}
|
||||||
|
|
||||||
const displayName = pack.display_name ?? room?.name ?? undefined;
|
constructor(eventId, content, room) {
|
||||||
const avatarUrl = pack.avatar_url ?? room?.getMxcAvatarUrl() ?? undefined;
|
this.id = eventId;
|
||||||
const packUsage = pack.usage ?? ['emoticon', 'sticker'];
|
this.content = JSON.parse(JSON.stringify(content));
|
||||||
const { attribution } = pack;
|
|
||||||
const images = new Map();
|
|
||||||
const emoticons = [];
|
|
||||||
const stickers = [];
|
|
||||||
|
|
||||||
Object.entries(packContent.images).forEach(([shortcode, data]) => {
|
this.displayName = room?.name ?? undefined;
|
||||||
|
this.avatarUrl = room?.getMxcAvatarUrl() ?? undefined;
|
||||||
|
|
||||||
|
this.applyPack(content);
|
||||||
|
this.applyImages(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyPack(content) {
|
||||||
|
const pack = content.pack ?? {};
|
||||||
|
|
||||||
|
this.displayName = pack.display_name ?? this.displayName;
|
||||||
|
this.avatarUrl = pack.avatar_url ?? this.avatarUrl;
|
||||||
|
this.usage = pack.usage ?? ['emoticon', 'sticker'];
|
||||||
|
this.attribution = pack.attribution;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyImages(content) {
|
||||||
|
this.images = new Map();
|
||||||
|
this.emoticons = [];
|
||||||
|
this.stickers = [];
|
||||||
|
|
||||||
|
Object.entries(content.images).forEach(([shortcode, data]) => {
|
||||||
const mxc = data.url;
|
const mxc = data.url;
|
||||||
const body = data.body ?? shortcode;
|
const body = data.body ?? shortcode;
|
||||||
const usage = data.usage ?? packUsage;
|
const usage = data.usage ?? this.usage;
|
||||||
const { info } = data;
|
const { info } = data;
|
||||||
|
|
||||||
if (!mxc) return;
|
if (!mxc) return;
|
||||||
|
@ -29,44 +47,14 @@ class ImagePack {
|
||||||
shortcode, mxc, body, usage, info,
|
shortcode, mxc, body, usage, info,
|
||||||
};
|
};
|
||||||
|
|
||||||
images.set(shortcode, image);
|
this.images.set(shortcode, image);
|
||||||
if (usage.includes('emoticon')) {
|
if (usage.includes('emoticon')) {
|
||||||
emoticons.push(image);
|
this.emoticons.push(image);
|
||||||
}
|
}
|
||||||
if (usage.includes('sticker')) {
|
if (usage.includes('sticker')) {
|
||||||
stickers.push(image);
|
this.stickers.push(image);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return new ImagePack(eventId, {
|
|
||||||
displayName,
|
|
||||||
avatarUrl,
|
|
||||||
usage: packUsage,
|
|
||||||
attribution,
|
|
||||||
images,
|
|
||||||
emoticons,
|
|
||||||
stickers,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(id, {
|
|
||||||
displayName,
|
|
||||||
avatarUrl,
|
|
||||||
usage,
|
|
||||||
attribution,
|
|
||||||
images,
|
|
||||||
emoticons,
|
|
||||||
stickers,
|
|
||||||
}) {
|
|
||||||
this.id = id;
|
|
||||||
this.displayName = displayName;
|
|
||||||
this.avatarUrl = avatarUrl;
|
|
||||||
this.usage = usage;
|
|
||||||
this.attribution = attribution;
|
|
||||||
|
|
||||||
this.images = images;
|
|
||||||
this.emoticons = emoticons;
|
|
||||||
this.stickers = stickers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getImages() {
|
getImages() {
|
||||||
|
@ -80,6 +68,80 @@ class ImagePack {
|
||||||
getStickers() {
|
getStickers() {
|
||||||
return this.stickers;
|
return this.stickers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getContent() {
|
||||||
|
return this.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
_updatePackProperty(property, value) {
|
||||||
|
if (this.content.pack === undefined) {
|
||||||
|
this.content.pack = {};
|
||||||
|
}
|
||||||
|
this.content.pack[property] = value;
|
||||||
|
this.applyPack(this.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAvatarUrl(avatarUrl) {
|
||||||
|
this._updatePackProperty('avatar_url', avatarUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisplayName(displayName) {
|
||||||
|
this._updatePackProperty('display_name', displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAttribution(attribution) {
|
||||||
|
this._updatePackProperty('attribution', attribution);
|
||||||
|
}
|
||||||
|
|
||||||
|
setUsage(usage) {
|
||||||
|
this._updatePackProperty('usage', usage);
|
||||||
|
}
|
||||||
|
|
||||||
|
addImage(key, imgContent) {
|
||||||
|
this.content.images = {
|
||||||
|
[key]: imgContent,
|
||||||
|
...this.content.images,
|
||||||
|
};
|
||||||
|
this.applyImages(this.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeImage(key) {
|
||||||
|
if (this.content.images[key] === undefined) return;
|
||||||
|
delete this.content.images[key];
|
||||||
|
this.applyImages(this.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateImageKey(key, newKey) {
|
||||||
|
if (this.content.images[key] === undefined) return;
|
||||||
|
const copyImages = {};
|
||||||
|
Object.keys(this.content.images).forEach((imgKey) => {
|
||||||
|
copyImages[imgKey === key ? newKey : imgKey] = this.content.images[imgKey];
|
||||||
|
});
|
||||||
|
this.content.images = copyImages;
|
||||||
|
this.applyImages(this.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateImageProperty(key, property, value) {
|
||||||
|
if (this.content.images[key] === undefined) return;
|
||||||
|
this.content.images[key][property] = value;
|
||||||
|
this.applyImages(this.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
setImageUrl(key, url) {
|
||||||
|
this._updateImageProperty(key, 'url', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
setImageBody(key, body) {
|
||||||
|
this._updateImageProperty(key, 'body', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
setImageInfo(key, info) {
|
||||||
|
this._updateImageProperty(key, 'info', info);
|
||||||
|
}
|
||||||
|
|
||||||
|
setImageUsage(key, usage) {
|
||||||
|
this._updateImageProperty(key, 'usage', usage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGlobalImagePacks(mx) {
|
function getGlobalImagePacks(mx) {
|
||||||
|
@ -112,7 +174,6 @@ function getUserImagePack(mx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const userImagePack = ImagePack.parsePack(mx.getUserId(), accountDataEmoji.event.content);
|
const userImagePack = ImagePack.parsePack(mx.getUserId(), accountDataEmoji.event.content);
|
||||||
if (userImagePack) userImagePack.displayName ??= 'Personal Emoji';
|
|
||||||
return userImagePack;
|
return userImagePack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,21 +5,10 @@ import encrypt from 'browser-encrypt-attachment';
|
||||||
import { math } from 'micromark-extension-math';
|
import { math } from 'micromark-extension-math';
|
||||||
import { getShortcodeToEmoji } from '../../app/organisms/emoji-board/custom-emoji';
|
import { getShortcodeToEmoji } from '../../app/organisms/emoji-board/custom-emoji';
|
||||||
import { mathExtensionHtml, spoilerExtension, spoilerExtensionHtml } from '../../util/markdown';
|
import { mathExtensionHtml, spoilerExtension, spoilerExtensionHtml } from '../../util/markdown';
|
||||||
|
import { getImageDimension } from '../../util/common';
|
||||||
import cons from './cons';
|
import cons from './cons';
|
||||||
import settings from './settings';
|
import settings from './settings';
|
||||||
|
|
||||||
function getImageDimension(file) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = async () => {
|
|
||||||
resolve({
|
|
||||||
w: img.width,
|
|
||||||
h: img.height,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
img.src = URL.createObjectURL(file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function loadVideo(videoFile) {
|
function loadVideo(videoFile) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const video = document.createElement('video');
|
const video = document.createElement('video');
|
||||||
|
|
|
@ -132,3 +132,62 @@ export function copyToClipboard(text) {
|
||||||
copyInput.remove();
|
copyInput.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function suffixRename(name, validator) {
|
||||||
|
let suffix = 2;
|
||||||
|
let newName = name;
|
||||||
|
do {
|
||||||
|
newName = name + suffix;
|
||||||
|
suffix += 1;
|
||||||
|
} while (validator(newName));
|
||||||
|
|
||||||
|
return newName;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getImageDimension(file) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = async () => {
|
||||||
|
resolve({
|
||||||
|
w: img.width,
|
||||||
|
h: img.height,
|
||||||
|
});
|
||||||
|
URL.revokeObjectURL(img.src);
|
||||||
|
};
|
||||||
|
img.src = URL.createObjectURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scaleDownImage(imageFile, width, height) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const imgURL = URL.createObjectURL(imageFile);
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
let newWidth = img.width;
|
||||||
|
let newHeight = img.height;
|
||||||
|
|
||||||
|
if (newHeight > height) {
|
||||||
|
newWidth = Math.floor(newWidth * (height / newHeight));
|
||||||
|
newHeight = height;
|
||||||
|
}
|
||||||
|
if (newWidth > width) {
|
||||||
|
newHeight = Math.floor(newHeight * (width / newWidth));
|
||||||
|
newWidth = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = newWidth;
|
||||||
|
canvas.height = newHeight;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.drawImage(img, 0, 0, newWidth, newHeight);
|
||||||
|
|
||||||
|
canvas.toBlob((thumbnail) => {
|
||||||
|
URL.revokeObjectURL(imgURL);
|
||||||
|
resolve(thumbnail);
|
||||||
|
}, imageFile.type);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.src = imgURL;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue