Remove old settings components (#2296)
This commit is contained in:
parent
82805dcfdd
commit
12ae94cd60
37 changed files with 6 additions and 3476 deletions
|
|
@ -1,511 +0,0 @@
|
||||||
import React, { useState, useMemo, useReducer, useEffect } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { EventTimeline } from 'matrix-js-sdk';
|
|
||||||
import './ImagePack.scss';
|
|
||||||
|
|
||||||
import { openReusableDialog } from '../../../client/action/navigation';
|
|
||||||
import { suffixRename } from '../../../util/common';
|
|
||||||
|
|
||||||
import Button from '../../atoms/button/Button';
|
|
||||||
import Text from '../../atoms/text/Text';
|
|
||||||
import Input from '../../atoms/input/Input';
|
|
||||||
import Checkbox from '../../atoms/button/Checkbox';
|
|
||||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
|
||||||
|
|
||||||
import { ImagePack as ImagePackBuilder } from '../../organisms/emoji-board/custom-emoji';
|
|
||||||
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
|
||||||
import ImagePackProfile from './ImagePackProfile';
|
|
||||||
import ImagePackItem from './ImagePackItem';
|
|
||||||
import ImagePackUpload from './ImagePackUpload';
|
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|
||||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
|
||||||
import { getStateEvent } from '../../utils/room';
|
|
||||||
|
|
||||||
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) {
|
|
||||||
if (usage.includes('emoticon') && usage.includes('sticker')) return 'both';
|
|
||||||
if (usage.includes('emoticon')) return 'emoticon';
|
|
||||||
if (usage.includes('sticker')) return 'sticker';
|
|
||||||
|
|
||||||
return 'both';
|
|
||||||
}
|
|
||||||
|
|
||||||
function isGlobalPack(mx, roomId, stateKey) {
|
|
||||||
const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent();
|
|
||||||
if (typeof globalContent !== 'object') return false;
|
|
||||||
|
|
||||||
const { rooms } = globalContent;
|
|
||||||
if (typeof rooms !== 'object') return false;
|
|
||||||
|
|
||||||
return rooms[roomId]?.[stateKey] !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function useRoomImagePack(roomId, stateKey) {
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
const room = mx.getRoom(roomId);
|
|
||||||
|
|
||||||
const pack = useMemo(() => {
|
|
||||||
const packEvent = getStateEvent(room, 'im.ponies.room_emotes', stateKey);
|
|
||||||
return ImagePackBuilder.parsePack(packEvent.getId(), packEvent.getContent());
|
|
||||||
}, [room, stateKey]);
|
|
||||||
|
|
||||||
const sendPackContent = (content) => {
|
|
||||||
mx.sendStateEvent(roomId, 'im.ponies.room_emotes', content, stateKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
pack,
|
|
||||||
sendPackContent,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function useUserImagePack() {
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
const pack = useMemo(() => {
|
|
||||||
const packEvent = mx.getAccountData('im.ponies.user_emotes');
|
|
||||||
return ImagePackBuilder.parsePack(
|
|
||||||
mx.getUserId(),
|
|
||||||
packEvent?.getContent() ?? {
|
|
||||||
pack: { display_name: 'Personal' },
|
|
||||||
images: {},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}, [mx]);
|
|
||||||
|
|
||||||
const sendPackContent = (content) => {
|
|
||||||
mx.setAccountData('im.ponies.user_emotes', content);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 handleAvatarChange = (url) => {
|
|
||||||
pack.setAvatarUrl(url);
|
|
||||||
sendPackContent(pack.getContent());
|
|
||||||
forceUpdate();
|
|
||||||
};
|
|
||||||
const handleEditProfile = (name, attribution) => {
|
|
||||||
pack.setDisplayName(name);
|
|
||||||
pack.setAttribution(attribution);
|
|
||||||
sendPackContent(pack.getContent());
|
|
||||||
forceUpdate();
|
|
||||||
};
|
|
||||||
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 {
|
|
||||||
handleAvatarChange,
|
|
||||||
handleEditProfile,
|
|
||||||
handleUsageChange,
|
|
||||||
handleRenameItem,
|
|
||||||
handleDeleteItem,
|
|
||||||
handleUsageItem,
|
|
||||||
handleAddItem,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function addGlobalImagePack(mx, roomId, stateKey) {
|
|
||||||
const content = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? {};
|
|
||||||
if (!content.rooms) content.rooms = {};
|
|
||||||
if (!content.rooms[roomId]) content.rooms[roomId] = {};
|
|
||||||
content.rooms[roomId][stateKey] = {};
|
|
||||||
return mx.setAccountData('im.ponies.emote_rooms', content);
|
|
||||||
}
|
|
||||||
function removeGlobalImagePack(mx, roomId, stateKey) {
|
|
||||||
const content = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? {};
|
|
||||||
if (!content.rooms) return Promise.resolve();
|
|
||||||
if (!content.rooms[roomId]) return Promise.resolve();
|
|
||||||
delete content.rooms[roomId][stateKey];
|
|
||||||
if (Object.keys(content.rooms[roomId]).length === 0) {
|
|
||||||
delete content.rooms[roomId];
|
|
||||||
}
|
|
||||||
return mx.setAccountData('im.ponies.emote_rooms', content);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ImagePack({ roomId, stateKey, handlePackDelete }) {
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
const room = mx.getRoom(roomId);
|
|
||||||
const [viewMore, setViewMore] = useState(false);
|
|
||||||
const [isGlobal, setIsGlobal] = useState(isGlobalPack(mx, roomId, stateKey));
|
|
||||||
const useAuthentication = useMediaAuthentication();
|
|
||||||
|
|
||||||
const { pack, sendPackContent } = useRoomImagePack(roomId, stateKey);
|
|
||||||
|
|
||||||
const {
|
|
||||||
handleAvatarChange,
|
|
||||||
handleEditProfile,
|
|
||||||
handleUsageChange,
|
|
||||||
handleRenameItem,
|
|
||||||
handleDeleteItem,
|
|
||||||
handleUsageItem,
|
|
||||||
handleAddItem,
|
|
||||||
} = useImagePackHandles(pack, sendPackContent);
|
|
||||||
|
|
||||||
const handleGlobalChange = (isG) => {
|
|
||||||
setIsGlobal(isG);
|
|
||||||
if (isG) addGlobalImagePack(mx, roomId, stateKey);
|
|
||||||
else removeGlobalImagePack(mx, roomId, stateKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
|
||||||
const canChange = room
|
|
||||||
.getLiveTimeline()
|
|
||||||
.getState(EventTimeline.FORWARDS)
|
|
||||||
?.hasSufficientPowerLevelFor('state_default', myPowerlevel);
|
|
||||||
|
|
||||||
const handleDeletePack = async () => {
|
|
||||||
const isConfirmed = await confirmDialog(
|
|
||||||
'Delete Pack',
|
|
||||||
`Are you sure that you want to delete "${pack.displayName}"?`,
|
|
||||||
'Delete',
|
|
||||||
'danger'
|
|
||||||
);
|
|
||||||
if (!isConfirmed) return;
|
|
||||||
handlePackDelete(stateKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
const images = [...pack.images].slice(0, viewMore ? pack.images.size : 2);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="image-pack">
|
|
||||||
<ImagePackProfile
|
|
||||||
avatarUrl={
|
|
||||||
pack.avatarUrl
|
|
||||||
? mx.mxcUrlToHttp(
|
|
||||||
pack.avatarUrl,
|
|
||||||
42,
|
|
||||||
42,
|
|
||||||
'crop',
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
useAuthentication
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
displayName={pack.displayName ?? 'Unknown'}
|
|
||||||
attribution={pack.attribution}
|
|
||||||
usage={getUsage(pack.usage)}
|
|
||||||
onUsageChange={canChange ? handleUsageChange : null}
|
|
||||||
onAvatarChange={canChange ? handleAvatarChange : null}
|
|
||||||
onEditProfile={canChange ? handleEditProfile : null}
|
|
||||||
/>
|
|
||||||
{canChange && <ImagePackUpload onUpload={handleAddItem} />}
|
|
||||||
{images.length === 0 ? null : (
|
|
||||||
<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,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
useAuthentication
|
|
||||||
)}
|
|
||||||
shortcode={shortcode}
|
|
||||||
usage={getUsage(image.usage)}
|
|
||||||
onUsageChange={canChange ? handleUsageItem : undefined}
|
|
||||||
onDelete={canChange ? handleDeleteItem : undefined}
|
|
||||||
onRename={canChange ? handleRenameItem : undefined}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{(pack.images.size > 2 || handlePackDelete) && (
|
|
||||||
<div className="image-pack__footer">
|
|
||||||
{pack.images.size > 2 && (
|
|
||||||
<Button onClick={() => setViewMore(!viewMore)}>
|
|
||||||
{viewMore ? 'View less' : `View ${pack.images.size - 2} more`}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{handlePackDelete && (
|
|
||||||
<Button variant="danger" onClick={handleDeletePack}>
|
|
||||||
Delete Pack
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="image-pack__global">
|
|
||||||
<Checkbox variant="positive" onToggle={handleGlobalChange} isActive={isGlobal} />
|
|
||||||
<div>
|
|
||||||
<Text variant="b2">Use globally</Text>
|
|
||||||
<Text variant="b3">Add this pack to your account to use in all rooms.</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImagePack.defaultProps = {
|
|
||||||
handlePackDelete: null,
|
|
||||||
};
|
|
||||||
ImagePack.propTypes = {
|
|
||||||
roomId: PropTypes.string.isRequired,
|
|
||||||
stateKey: PropTypes.string.isRequired,
|
|
||||||
handlePackDelete: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
function ImagePackUser() {
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
const [viewMore, setViewMore] = useState(false);
|
|
||||||
const useAuthentication = useMediaAuthentication();
|
|
||||||
|
|
||||||
const { pack, sendPackContent } = useUserImagePack();
|
|
||||||
|
|
||||||
const {
|
|
||||||
handleAvatarChange,
|
|
||||||
handleEditProfile,
|
|
||||||
handleUsageChange,
|
|
||||||
handleRenameItem,
|
|
||||||
handleDeleteItem,
|
|
||||||
handleUsageItem,
|
|
||||||
handleAddItem,
|
|
||||||
} = useImagePackHandles(pack, sendPackContent);
|
|
||||||
|
|
||||||
const images = [...pack.images].slice(0, viewMore ? pack.images.size : 2);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="image-pack">
|
|
||||||
<ImagePackProfile
|
|
||||||
avatarUrl={
|
|
||||||
pack.avatarUrl
|
|
||||||
? mx.mxcUrlToHttp(
|
|
||||||
pack.avatarUrl,
|
|
||||||
42,
|
|
||||||
42,
|
|
||||||
'crop',
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
useAuthentication
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
displayName={pack.displayName ?? 'Personal'}
|
|
||||||
attribution={pack.attribution}
|
|
||||||
usage={getUsage(pack.usage)}
|
|
||||||
onUsageChange={handleUsageChange}
|
|
||||||
onAvatarChange={handleAvatarChange}
|
|
||||||
onEditProfile={handleEditProfile}
|
|
||||||
/>
|
|
||||||
<ImagePackUpload onUpload={handleAddItem} />
|
|
||||||
{images.length === 0 ? null : (
|
|
||||||
<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,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
useAuthentication
|
|
||||||
)}
|
|
||||||
shortcode={shortcode}
|
|
||||||
usage={getUsage(image.usage)}
|
|
||||||
onUsageChange={handleUsageItem}
|
|
||||||
onDelete={handleDeleteItem}
|
|
||||||
onRename={handleRenameItem}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{pack.images.size > 2 && (
|
|
||||||
<div className="image-pack__footer">
|
|
||||||
<Button onClick={() => setViewMore(!viewMore)}>
|
|
||||||
{viewMore ? 'View less' : `View ${pack.images.size - 2} more`}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function useGlobalImagePack() {
|
|
||||||
const [, forceUpdate] = useReducer((count) => count + 1, 0);
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
|
|
||||||
const roomIdToStateKeys = new Map();
|
|
||||||
const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? { rooms: {} };
|
|
||||||
const { rooms } = globalContent;
|
|
||||||
|
|
||||||
Object.keys(rooms).forEach((roomId) => {
|
|
||||||
if (typeof rooms[roomId] !== 'object') return;
|
|
||||||
const room = mx.getRoom(roomId);
|
|
||||||
const stateKeys = Object.keys(rooms[roomId]);
|
|
||||||
if (!room || stateKeys.length === 0) return;
|
|
||||||
roomIdToStateKeys.set(roomId, stateKeys);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleEvent = (event) => {
|
|
||||||
if (event.getType() === 'im.ponies.emote_rooms') forceUpdate();
|
|
||||||
};
|
|
||||||
mx.addListener('accountData', handleEvent);
|
|
||||||
return () => {
|
|
||||||
mx.removeListener('accountData', handleEvent);
|
|
||||||
};
|
|
||||||
}, [mx]);
|
|
||||||
|
|
||||||
return roomIdToStateKeys;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ImagePackGlobal() {
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
const roomIdToStateKeys = useGlobalImagePack();
|
|
||||||
|
|
||||||
const handleChange = (roomId, stateKey) => {
|
|
||||||
removeGlobalImagePack(mx, roomId, stateKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="image-pack-global">
|
|
||||||
<MenuHeader>Global packs</MenuHeader>
|
|
||||||
<div>
|
|
||||||
{roomIdToStateKeys.size > 0 ? (
|
|
||||||
[...roomIdToStateKeys].map(([roomId, stateKeys]) => {
|
|
||||||
const room = mx.getRoom(roomId);
|
|
||||||
return stateKeys.map((stateKey) => {
|
|
||||||
const data = getStateEvent(room, 'im.ponies.room_emotes', stateKey);
|
|
||||||
const pack = ImagePackBuilder.parsePack(data?.getId(), data?.getContent());
|
|
||||||
if (!pack) return null;
|
|
||||||
return (
|
|
||||||
<div className="image-pack__global" key={pack.id}>
|
|
||||||
<Checkbox
|
|
||||||
variant="positive"
|
|
||||||
onToggle={() => handleChange(roomId, stateKey)}
|
|
||||||
isActive
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<Text variant="b2">{pack.displayName ?? 'Unknown'}</Text>
|
|
||||||
<Text variant="b3">{room.name}</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<div className="image-pack-global__empty">
|
|
||||||
<Text>No global packs</Text>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ImagePack;
|
|
||||||
|
|
||||||
export { ImagePackUser, ImagePackGlobal };
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
@use '../../partials/flex';
|
|
||||||
|
|
||||||
.image-pack {
|
|
||||||
&-item {
|
|
||||||
border-top: 1px solid var(--bg-surface-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__header {
|
|
||||||
padding: var(--sp-extra-tight) var(--sp-normal);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-normal);
|
|
||||||
|
|
||||||
& > *:nth-child(2) {
|
|
||||||
@extend .cp-fx__item-one;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__footer {
|
|
||||||
padding: var(--sp-normal);
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--sp-tight);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__global {
|
|
||||||
padding: var(--sp-normal);
|
|
||||||
padding-top: var(--sp-tight);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-normal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-pack-global {
|
|
||||||
&__empty {
|
|
||||||
text-align: center;
|
|
||||||
padding: var(--sp-extra-loose) var(--sp-normal);
|
|
||||||
}
|
|
||||||
& .image-pack__global {
|
|
||||||
padding: 0 var(--sp-normal);
|
|
||||||
padding-bottom: var(--sp-normal);
|
|
||||||
&:first-child {
|
|
||||||
padding-top: var(--sp-normal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './ImagePackItem.scss';
|
|
||||||
|
|
||||||
import { openReusableContextMenu } from '../../../client/action/navigation';
|
|
||||||
import { getEventCords } from '../../../util/common';
|
|
||||||
|
|
||||||
import Avatar from '../../atoms/avatar/Avatar';
|
|
||||||
import Text from '../../atoms/text/Text';
|
|
||||||
import Button from '../../atoms/button/Button';
|
|
||||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
|
||||||
import IconButton from '../../atoms/button/IconButton';
|
|
||||||
import ImagePackUsageSelector from './ImagePackUsageSelector';
|
|
||||||
|
|
||||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
|
||||||
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
|
||||||
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
|
||||||
|
|
||||||
function ImagePackItem({
|
|
||||||
url, shortcode, usage, onUsageChange, onDelete, onRename,
|
|
||||||
}) {
|
|
||||||
const handleUsageSelect = (event) => {
|
|
||||||
openReusableContextMenu(
|
|
||||||
'bottom',
|
|
||||||
getEventCords(event, '.btn-surface'),
|
|
||||||
(closeMenu) => (
|
|
||||||
<ImagePackUsageSelector
|
|
||||||
usage={usage}
|
|
||||||
onSelect={(newUsage) => {
|
|
||||||
onUsageChange(shortcode, newUsage);
|
|
||||||
closeMenu();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="image-pack-item">
|
|
||||||
<Avatar imageSrc={url} size="extra-small" text={shortcode} bgColor="black" />
|
|
||||||
<div className="image-pack-item__content">
|
|
||||||
<Text>{shortcode}</Text>
|
|
||||||
</div>
|
|
||||||
<div className="image-pack-item__usage">
|
|
||||||
<div className="image-pack-item__btn">
|
|
||||||
{onRename && <IconButton tooltip="Rename" size="extra-small" src={PencilIC} onClick={() => onRename(shortcode)} />}
|
|
||||||
{onDelete && <IconButton tooltip="Delete" size="extra-small" src={BinIC} onClick={() => onDelete(shortcode)} />}
|
|
||||||
</div>
|
|
||||||
<Button onClick={onUsageChange ? handleUsageSelect : undefined}>
|
|
||||||
{onUsageChange && <RawIcon src={ChevronBottomIC} size="extra-small" />}
|
|
||||||
<Text variant="b2">
|
|
||||||
{usage === 'emoticon' && 'Emoji'}
|
|
||||||
{usage === 'sticker' && 'Sticker'}
|
|
||||||
{usage === 'both' && 'Both'}
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImagePackItem.defaultProps = {
|
|
||||||
onUsageChange: null,
|
|
||||||
onDelete: null,
|
|
||||||
onRename: null,
|
|
||||||
};
|
|
||||||
ImagePackItem.propTypes = {
|
|
||||||
url: PropTypes.string.isRequired,
|
|
||||||
shortcode: PropTypes.string.isRequired,
|
|
||||||
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
|
||||||
onUsageChange: PropTypes.func,
|
|
||||||
onDelete: PropTypes.func,
|
|
||||||
onRename: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImagePackItem;
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
@use '../../partials/flex';
|
|
||||||
@use '../../partials/dir';
|
|
||||||
|
|
||||||
.image-pack-item {
|
|
||||||
margin: 0 var(--sp-normal);
|
|
||||||
padding: var(--sp-tight) 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-normal);
|
|
||||||
|
|
||||||
& .avatar-container img {
|
|
||||||
object-fit: contain;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
@extend .cp-fx__item-one;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__usage {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--sp-ultra-tight);
|
|
||||||
& button {
|
|
||||||
padding: 6px;
|
|
||||||
}
|
|
||||||
& > button.btn-surface {
|
|
||||||
padding: 6px var(--sp-tight);
|
|
||||||
min-width: 0;
|
|
||||||
@include dir.side(margin, var(--sp-ultra-tight), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__btn {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
&:hover,
|
|
||||||
&:focus-within {
|
|
||||||
.image-pack-item__btn {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--sp-ultra-tight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
import React, { useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './ImagePackProfile.scss';
|
|
||||||
|
|
||||||
import { openReusableContextMenu } from '../../../client/action/navigation';
|
|
||||||
import { getEventCords } from '../../../util/common';
|
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
|
||||||
import Avatar from '../../atoms/avatar/Avatar';
|
|
||||||
import Button from '../../atoms/button/Button';
|
|
||||||
import IconButton from '../../atoms/button/IconButton';
|
|
||||||
import Input from '../../atoms/input/Input';
|
|
||||||
import ImageUpload from '../image-upload/ImageUpload';
|
|
||||||
import ImagePackUsageSelector from './ImagePackUsageSelector';
|
|
||||||
|
|
||||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
|
||||||
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
|
||||||
|
|
||||||
function ImagePackProfile({
|
|
||||||
avatarUrl, displayName, attribution, usage,
|
|
||||||
onUsageChange, onAvatarChange, onEditProfile,
|
|
||||||
}) {
|
|
||||||
const [isEdit, setIsEdit] = useState(false);
|
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const { nameInput, attributionInput } = e.target;
|
|
||||||
const name = nameInput.value.trim() || undefined;
|
|
||||||
const att = attributionInput.value.trim() || undefined;
|
|
||||||
|
|
||||||
onEditProfile(name, att);
|
|
||||||
setIsEdit(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUsageSelect = (event) => {
|
|
||||||
openReusableContextMenu(
|
|
||||||
'bottom',
|
|
||||||
getEventCords(event, '.btn-surface'),
|
|
||||||
(closeMenu) => (
|
|
||||||
<ImagePackUsageSelector
|
|
||||||
usage={usage}
|
|
||||||
onSelect={(newUsage) => {
|
|
||||||
onUsageChange(newUsage);
|
|
||||||
closeMenu();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="image-pack-profile">
|
|
||||||
{
|
|
||||||
onAvatarChange
|
|
||||||
? (
|
|
||||||
<ImageUpload
|
|
||||||
bgColor="#555"
|
|
||||||
text={displayName}
|
|
||||||
imageSrc={avatarUrl}
|
|
||||||
size="normal"
|
|
||||||
onUpload={onAvatarChange}
|
|
||||||
onRequestRemove={() => onAvatarChange(undefined)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
: <Avatar bgColor="#555" text={displayName} imageSrc={avatarUrl} size="normal" />
|
|
||||||
}
|
|
||||||
<div className="image-pack-profile__content">
|
|
||||||
{
|
|
||||||
isEdit
|
|
||||||
? (
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<Input name="nameInput" label="Name" value={displayName} required />
|
|
||||||
<Input name="attributionInput" label="Attribution" value={attribution} resizable />
|
|
||||||
<div>
|
|
||||||
<Button variant="primary" type="submit">Save</Button>
|
|
||||||
<Button onClick={() => setIsEdit(false)}>Cancel</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<Text>{displayName}</Text>
|
|
||||||
{onEditProfile && <IconButton size="extra-small" onClick={() => setIsEdit(true)} src={PencilIC} tooltip="Edit" />}
|
|
||||||
</div>
|
|
||||||
{attribution && <Text variant="b3">{attribution}</Text>}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div className="image-pack-profile__usage">
|
|
||||||
<Text variant="b3">Pack usage</Text>
|
|
||||||
<Button
|
|
||||||
onClick={onUsageChange ? handleUsageSelect : undefined}
|
|
||||||
iconSrc={onUsageChange ? ChevronBottomIC : null}
|
|
||||||
>
|
|
||||||
<Text>
|
|
||||||
{usage === 'emoticon' && 'Emoji'}
|
|
||||||
{usage === 'sticker' && 'Sticker'}
|
|
||||||
{usage === 'both' && 'Both'}
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImagePackProfile.defaultProps = {
|
|
||||||
avatarUrl: null,
|
|
||||||
attribution: null,
|
|
||||||
onUsageChange: null,
|
|
||||||
onAvatarChange: null,
|
|
||||||
onEditProfile: null,
|
|
||||||
};
|
|
||||||
ImagePackProfile.propTypes = {
|
|
||||||
avatarUrl: PropTypes.string,
|
|
||||||
displayName: PropTypes.string.isRequired,
|
|
||||||
attribution: PropTypes.string,
|
|
||||||
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
|
||||||
onUsageChange: PropTypes.func,
|
|
||||||
onAvatarChange: PropTypes.func,
|
|
||||||
onEditProfile: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImagePackProfile;
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
@use '../../partials/flex';
|
|
||||||
|
|
||||||
.image-pack-profile {
|
|
||||||
padding: var(--sp-normal);
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: var(--sp-tight);
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
@extend .cp-fx__item-one;
|
|
||||||
|
|
||||||
& > div:first-child {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-extra-tight);
|
|
||||||
|
|
||||||
& .ic-btn {
|
|
||||||
padding: var(--sp-ultra-tight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
& > form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--sp-extra-tight);
|
|
||||||
& > div:last-child {
|
|
||||||
margin: var(--sp-extra-tight) 0;
|
|
||||||
display: flex;
|
|
||||||
gap: var(--sp-tight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&__usage {
|
|
||||||
& > *:first-child {
|
|
||||||
margin-bottom: var(--sp-ultra-tight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
import React, { useState, useRef } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './ImagePackUpload.scss';
|
|
||||||
|
|
||||||
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';
|
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|
||||||
|
|
||||||
function ImagePackUpload({ onUpload }) {
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
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 { content_uri: url } = await mx.uploadContent(image);
|
|
||||||
|
|
||||||
onUpload(shortcode, url);
|
|
||||||
setProgress(false);
|
|
||||||
setImgFile(null);
|
|
||||||
shortcodeRef.current.value = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileChange = (evt) => {
|
|
||||||
const img = evt.target.files[0];
|
|
||||||
if (!img) return;
|
|
||||||
setImgFile(img);
|
|
||||||
shortcodeRef.current.value = img.name.slice(0, img.name.indexOf('.'));
|
|
||||||
shortcodeRef.current.focus();
|
|
||||||
};
|
|
||||||
const handleRemove = () => {
|
|
||||||
setImgFile(null);
|
|
||||||
inputRef.current.value = null;
|
|
||||||
shortcodeRef.current.value = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
|
||||||
import CheckIC from '../../../../public/res/ic/outlined/check.svg';
|
|
||||||
|
|
||||||
function ImagePackUsageSelector({ usage, onSelect }) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<MenuHeader>Usage</MenuHeader>
|
|
||||||
<MenuItem
|
|
||||||
iconSrc={usage === 'emoticon' ? CheckIC : undefined}
|
|
||||||
variant={usage === 'emoticon' ? 'positive' : 'surface'}
|
|
||||||
onClick={() => onSelect('emoticon')}
|
|
||||||
>
|
|
||||||
Emoji
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
iconSrc={usage === 'sticker' ? CheckIC : undefined}
|
|
||||||
variant={usage === 'sticker' ? 'positive' : 'surface'}
|
|
||||||
onClick={() => onSelect('sticker')}
|
|
||||||
>
|
|
||||||
Sticker
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
iconSrc={usage === 'both' ? CheckIC : undefined}
|
|
||||||
variant={usage === 'both' ? 'positive' : 'surface'}
|
|
||||||
onClick={() => onSelect('both')}
|
|
||||||
>
|
|
||||||
Both
|
|
||||||
</MenuItem>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImagePackUsageSelector.propTypes = {
|
|
||||||
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
|
||||||
onSelect: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImagePackUsageSelector;
|
|
||||||
|
|
@ -1,359 +0,0 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './RoomAliases.scss';
|
|
||||||
import { EventTimeline } from 'matrix-js-sdk';
|
|
||||||
|
|
||||||
import cons from '../../../client/state/cons';
|
|
||||||
import { Debounce } from '../../../util/common';
|
|
||||||
import { isRoomAliasAvailable } from '../../../util/matrixUtil';
|
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
|
||||||
import Button from '../../atoms/button/Button';
|
|
||||||
import Input from '../../atoms/input/Input';
|
|
||||||
import Checkbox from '../../atoms/button/Checkbox';
|
|
||||||
import Toggle from '../../atoms/button/Toggle';
|
|
||||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
|
||||||
import SettingTile from '../setting-tile/SettingTile';
|
|
||||||
|
|
||||||
import { useStore } from '../../hooks/useStore';
|
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|
||||||
|
|
||||||
function useValidate(hsString) {
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
const [debounce] = useState(new Debounce());
|
|
||||||
const [validate, setValidate] = useState({ alias: null, status: cons.status.PRE_FLIGHT });
|
|
||||||
|
|
||||||
const setValidateToDefault = () => {
|
|
||||||
setValidate({
|
|
||||||
alias: null,
|
|
||||||
status: cons.status.PRE_FLIGHT,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkValueOK = (value) => {
|
|
||||||
if (value.trim() === '') {
|
|
||||||
setValidateToDefault();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!value.match(/^[a-zA-Z0-9_-]+$/)) {
|
|
||||||
setValidate({
|
|
||||||
alias: null,
|
|
||||||
status: cons.status.ERROR,
|
|
||||||
msg: 'Invalid character: only letter, numbers and _- are allowed.',
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAliasChange = (e) => {
|
|
||||||
const input = e.target;
|
|
||||||
if (validate.status !== cons.status.PRE_FLIGHT) {
|
|
||||||
setValidateToDefault();
|
|
||||||
}
|
|
||||||
if (checkValueOK(input.value) === false) return;
|
|
||||||
|
|
||||||
debounce._(async () => {
|
|
||||||
const { value } = input;
|
|
||||||
const alias = `#${value}:${hsString}`;
|
|
||||||
if (checkValueOK(value) === false) return;
|
|
||||||
|
|
||||||
setValidate({
|
|
||||||
alias,
|
|
||||||
status: cons.status.IN_FLIGHT,
|
|
||||||
msg: `validating ${alias}...`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isValid = await isRoomAliasAvailable(mx, alias);
|
|
||||||
setValidate(() => {
|
|
||||||
if (e.target.value !== value) {
|
|
||||||
return { alias: null, status: cons.status.PRE_FLIGHT };
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
alias,
|
|
||||||
status: isValid ? cons.status.SUCCESS : cons.status.ERROR,
|
|
||||||
msg: isValid ? `${alias} is available.` : `${alias} is already in use.`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, 600)();
|
|
||||||
};
|
|
||||||
|
|
||||||
return [validate, setValidateToDefault, handleAliasChange];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAliases(mx, roomId) {
|
|
||||||
const room = mx.getRoom(roomId);
|
|
||||||
|
|
||||||
const main = room.getCanonicalAlias();
|
|
||||||
const published = room.getAltAliases();
|
|
||||||
if (main && !published.includes(main)) published.splice(0, 0, main);
|
|
||||||
|
|
||||||
return {
|
|
||||||
main,
|
|
||||||
published: [...new Set(published)],
|
|
||||||
local: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function RoomAliases({ roomId }) {
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
const room = mx.getRoom(roomId);
|
|
||||||
const userId = mx.getUserId();
|
|
||||||
const hsString = userId.slice(userId.indexOf(':') + 1);
|
|
||||||
|
|
||||||
const isMountedStore = useStore();
|
|
||||||
const [isPublic, setIsPublic] = useState(false);
|
|
||||||
const [isLocalVisible, setIsLocalVisible] = useState(false);
|
|
||||||
const [aliases, setAliases] = useState(getAliases(mx, roomId));
|
|
||||||
const [selectedAlias, setSelectedAlias] = useState(null);
|
|
||||||
const [deleteAlias, setDeleteAlias] = useState(null);
|
|
||||||
const [validate, setValidateToDefault, handleAliasChange] = useValidate(hsString);
|
|
||||||
|
|
||||||
const canPublishAlias = room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.maySendStateEvent('m.room.canonical_alias', userId);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
isMountedStore.setItem(true)
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let isUnmounted = false;
|
|
||||||
|
|
||||||
const loadLocalAliases = async () => {
|
|
||||||
let local = [];
|
|
||||||
try {
|
|
||||||
const result = await mx.getLocalAliases(roomId);
|
|
||||||
local = result.aliases.filter((alias) => !aliases.published.includes(alias));
|
|
||||||
} catch {
|
|
||||||
local = [];
|
|
||||||
}
|
|
||||||
aliases.local = [...new Set(local.reverse())];
|
|
||||||
|
|
||||||
if (isUnmounted) return;
|
|
||||||
setAliases({ ...aliases });
|
|
||||||
};
|
|
||||||
const loadVisibility = async () => {
|
|
||||||
const result = await mx.getRoomDirectoryVisibility(roomId);
|
|
||||||
if (isUnmounted) return;
|
|
||||||
setIsPublic(result.visibility === 'public');
|
|
||||||
};
|
|
||||||
loadLocalAliases();
|
|
||||||
loadVisibility();
|
|
||||||
return () => {
|
|
||||||
isUnmounted = true;
|
|
||||||
};
|
|
||||||
}, [mx, roomId]);
|
|
||||||
|
|
||||||
const toggleDirectoryVisibility = () => {
|
|
||||||
mx.setRoomDirectoryVisibility(roomId, isPublic ? 'private' : 'public');
|
|
||||||
setIsPublic(!isPublic);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAliasSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (validate.status === cons.status.ERROR) return;
|
|
||||||
if (!validate.alias) return;
|
|
||||||
|
|
||||||
const { alias } = validate;
|
|
||||||
const aliasInput = e.target.elements['alias-input'];
|
|
||||||
aliasInput.value = '';
|
|
||||||
setValidateToDefault();
|
|
||||||
|
|
||||||
try {
|
|
||||||
aliases.local.push(alias);
|
|
||||||
setAliases({ ...aliases });
|
|
||||||
await mx.createAlias(alias, roomId);
|
|
||||||
} catch {
|
|
||||||
if (isMountedStore.getItem()) {
|
|
||||||
const lIndex = alias.local.indexOf(alias);
|
|
||||||
if (lIndex === -1) return;
|
|
||||||
aliases.local.splice(lIndex, 1);
|
|
||||||
setAliases({ ...aliases });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAliasSelect = (alias) => {
|
|
||||||
setSelectedAlias(alias === selectedAlias ? null : alias);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePublishAlias = (alias) => {
|
|
||||||
const { main, published } = aliases;
|
|
||||||
let { local } = aliases;
|
|
||||||
|
|
||||||
if (!published.includes(aliases)) {
|
|
||||||
published.push(alias);
|
|
||||||
local = local.filter((al) => al !== alias);
|
|
||||||
mx.sendStateEvent(roomId, 'm.room.canonical_alias', {
|
|
||||||
alias: main,
|
|
||||||
alt_aliases: published.filter((al) => al !== main),
|
|
||||||
});
|
|
||||||
setAliases({ main, published, local });
|
|
||||||
setSelectedAlias(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUnPublishAlias = (alias) => {
|
|
||||||
let { main, published } = aliases;
|
|
||||||
const { local } = aliases;
|
|
||||||
|
|
||||||
if (published.includes(alias) || main === alias) {
|
|
||||||
if (main === alias) main = null;
|
|
||||||
published = published.filter((al) => al !== alias);
|
|
||||||
local.push(alias);
|
|
||||||
mx.sendStateEvent(roomId, 'm.room.canonical_alias', {
|
|
||||||
alias: main,
|
|
||||||
alt_aliases: published.filter((al) => al !== main),
|
|
||||||
});
|
|
||||||
setAliases({ main, published, local });
|
|
||||||
setSelectedAlias(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSetMainAlias = (alias) => {
|
|
||||||
let { main, local } = aliases;
|
|
||||||
const { published } = aliases;
|
|
||||||
|
|
||||||
if (main !== alias) {
|
|
||||||
main = alias;
|
|
||||||
if (!published.includes(alias)) published.splice(0, 0, alias);
|
|
||||||
local = local.filter((al) => al !== alias);
|
|
||||||
mx.sendStateEvent(roomId, 'm.room.canonical_alias', {
|
|
||||||
alias: main,
|
|
||||||
alt_aliases: published.filter((al) => al !== main),
|
|
||||||
});
|
|
||||||
setAliases({ main, published, local });
|
|
||||||
setSelectedAlias(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteAlias = async (alias) => {
|
|
||||||
try {
|
|
||||||
setDeleteAlias({ alias, status: cons.status.IN_FLIGHT, msg: 'deleting...' });
|
|
||||||
await mx.deleteAlias(alias);
|
|
||||||
let { main, published, local } = aliases;
|
|
||||||
if (published.includes(alias)) {
|
|
||||||
handleUnPublishAlias(alias);
|
|
||||||
if (main === alias) main = null;
|
|
||||||
published = published.filter((al) => al !== alias);
|
|
||||||
}
|
|
||||||
|
|
||||||
local = local.filter((al) => al !== alias);
|
|
||||||
setAliases({ main, published, local });
|
|
||||||
setDeleteAlias(null);
|
|
||||||
setSelectedAlias(null);
|
|
||||||
} catch (err) {
|
|
||||||
setDeleteAlias({ alias, status: cons.status.ERROR, msg: err.message });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderAliasBtns = (alias) => {
|
|
||||||
const isPublished = aliases.published.includes(alias);
|
|
||||||
const isMain = aliases.main === alias;
|
|
||||||
if (deleteAlias?.alias === alias) {
|
|
||||||
const isError = deleteAlias.status === cons.status.ERROR;
|
|
||||||
return (
|
|
||||||
<div className="room-aliases__item-btns">
|
|
||||||
<Text variant="b2">
|
|
||||||
<span style={{ color: isError ? 'var(--tc-danger-high' : 'inherit' }}>{deleteAlias.msg}</span>
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="room-aliases__item-btns">
|
|
||||||
{canPublishAlias && !isMain && <Button onClick={() => handleSetMainAlias(alias)} variant="primary">Set as Main</Button>}
|
|
||||||
{!isPublished && canPublishAlias && <Button onClick={() => handlePublishAlias(alias)} variant="positive">Publish</Button>}
|
|
||||||
{isPublished && canPublishAlias && <Button onClick={() => handleUnPublishAlias(alias)} variant="caution">Un-Publish</Button>}
|
|
||||||
<Button onClick={() => handleDeleteAlias(alias)} variant="danger">Delete</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderAlias = (alias) => {
|
|
||||||
const isActive = selectedAlias === alias;
|
|
||||||
const disabled = !canPublishAlias && aliases.published.includes(alias);
|
|
||||||
const isMain = aliases.main === alias;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment key={`${alias}-wrapper`}>
|
|
||||||
<div className="room-aliases__alias-item" key={alias}>
|
|
||||||
<Checkbox variant="positive" disabled={disabled} isActive={isActive} onToggle={() => handleAliasSelect(alias)} />
|
|
||||||
<Text>
|
|
||||||
{alias}
|
|
||||||
{isMain && <span>Main</span>}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
{isActive && renderAliasBtns(alias)}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
let inputState = 'normal';
|
|
||||||
if (validate.status === cons.status.ERROR) inputState = 'error';
|
|
||||||
if (validate.status === cons.status.SUCCESS) inputState = 'success';
|
|
||||||
return (
|
|
||||||
<div className="room-aliases">
|
|
||||||
<SettingTile
|
|
||||||
title="Publish to room directory"
|
|
||||||
content={<Text variant="b3">{`Publish this ${room.isSpaceRoom() ? 'space' : 'room'} to the ${hsString}'s public room directory?`}</Text>}
|
|
||||||
options={(
|
|
||||||
<Toggle
|
|
||||||
isActive={isPublic}
|
|
||||||
onToggle={toggleDirectoryVisibility}
|
|
||||||
disabled={!canPublishAlias}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="room-aliases__content">
|
|
||||||
<MenuHeader>Published addresses</MenuHeader>
|
|
||||||
{(aliases.published.length === 0) && <Text className="room-aliases__message">No published addresses</Text>}
|
|
||||||
{(aliases.published.length > 0 && !aliases.main) && <Text className="room-aliases__message">No Main address (select one from below)</Text>}
|
|
||||||
{aliases.published.map(renderAlias)}
|
|
||||||
<Text className="room-aliases__message" variant="b3">
|
|
||||||
{`Published addresses can be used by anyone on any server to join your ${room.isSpaceRoom() ? 'space' : 'room'}. To publish an address, it needs to be set as a local address first.`}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
{ isLocalVisible && (
|
|
||||||
<div className="room-aliases__content">
|
|
||||||
<MenuHeader>Local addresses</MenuHeader>
|
|
||||||
{(aliases.local.length === 0) && <Text className="room-aliases__message">No local addresses</Text>}
|
|
||||||
{aliases.local.map(renderAlias)}
|
|
||||||
<Text className="room-aliases__message" variant="b3">
|
|
||||||
{`Set local addresses for this ${room.isSpaceRoom() ? 'space' : 'room'} so users can find this ${room.isSpaceRoom() ? 'space' : 'room'} through your homeserver.`}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="room-aliases__form-label" variant="b2">Add local address</Text>
|
|
||||||
<form className="room-aliases__form" onSubmit={handleAliasSubmit}>
|
|
||||||
<div className="room-aliases__input-wrapper">
|
|
||||||
<Input
|
|
||||||
name="alias-input"
|
|
||||||
state={inputState}
|
|
||||||
onChange={handleAliasChange}
|
|
||||||
placeholder={`my_${room.isSpaceRoom() ? 'space' : 'room'}_address`}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button variant="primary" type="submit">Add</Button>
|
|
||||||
</form>
|
|
||||||
<div className="room-aliases__input-status">
|
|
||||||
{validate.status === cons.status.SUCCESS && <Text className="room-aliases__valid" variant="b2">{validate.msg}</Text>}
|
|
||||||
{validate.status === cons.status.ERROR && <Text className="room-aliases__invalid" variant="b2">{validate.msg}</Text>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="room-aliases__content">
|
|
||||||
<Button onClick={() => setIsLocalVisible(!isLocalVisible)}>
|
|
||||||
{`${isLocalVisible ? 'Hide' : 'Add / View'} local address`}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
RoomAliases.propTypes = {
|
|
||||||
roomId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RoomAliases;
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
@use '../../partials/flex';
|
|
||||||
@use '../../partials/dir';
|
|
||||||
@use '../../partials/text';
|
|
||||||
|
|
||||||
.room-aliases {
|
|
||||||
&__message,
|
|
||||||
& .setting-tile {
|
|
||||||
margin: var(--sp-tight) var(--sp-normal);
|
|
||||||
}
|
|
||||||
& .setting-tile {
|
|
||||||
margin-bottom: var(--sp-loose);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__alias-item {
|
|
||||||
padding: var(--sp-extra-tight) var(--sp-normal);
|
|
||||||
@extend .cp-fx__row--s-c;
|
|
||||||
&.checkbox {
|
|
||||||
@include dir.side(margin, 0 , var(--sp-tight));
|
|
||||||
}
|
|
||||||
& .text {
|
|
||||||
@extend .cp-fx__item-one;
|
|
||||||
@extend .cp-txt__ellipsis;
|
|
||||||
color: var(--tc-surface-high);
|
|
||||||
span {
|
|
||||||
margin: 0 var(--sp-extra-tight);
|
|
||||||
padding: 0 var(--sp-ultra-tight);
|
|
||||||
color: var(--bg-surface);
|
|
||||||
background-color: var(--tc-surface-low);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&__item-btns {
|
|
||||||
@include dir.side(margin, 48px, 0);
|
|
||||||
& button {
|
|
||||||
padding: var(--sp-ultra-tight) var(--sp-tight);
|
|
||||||
margin-bottom: var(--sp-tight);
|
|
||||||
@include dir.side(margin, 0, var(--sp-tight));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
margin-bottom: var(--sp-normal);
|
|
||||||
|
|
||||||
& .checkbox {
|
|
||||||
@include dir.side(margin, 0, var(--sp-tight));
|
|
||||||
min-width: 20px;
|
|
||||||
}
|
|
||||||
& > button {
|
|
||||||
margin: 0 var(--sp-normal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__form {
|
|
||||||
padding: var(--sp-normal);
|
|
||||||
padding-top: 0;
|
|
||||||
display: flex;
|
|
||||||
&-label {
|
|
||||||
padding: var(--sp-normal) var(--sp-normal) var(--sp-ultra-tight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__input-wrapper {
|
|
||||||
display: flex;
|
|
||||||
@extend .cp-fx__item-one;
|
|
||||||
@include dir.side(margin, 0, var(--sp-tight));
|
|
||||||
|
|
||||||
& .input-container {
|
|
||||||
@extend .cp-fx__item-one;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__input-status {
|
|
||||||
padding: 0 var(--sp-normal);
|
|
||||||
}
|
|
||||||
&__valid {
|
|
||||||
color: var(--tc-positive-high);
|
|
||||||
padding-bottom: var(--sp-normal);
|
|
||||||
}
|
|
||||||
&__invalid {
|
|
||||||
color: var(--tc-danger-high);
|
|
||||||
padding-bottom: var(--sp-normal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
import React, { useReducer, useEffect } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './RoomEmojis.scss';
|
|
||||||
import { EventTimeline } from 'matrix-js-sdk';
|
|
||||||
|
|
||||||
import { suffixRename } from '../../../util/common';
|
|
||||||
|
|
||||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
|
||||||
import Text from '../../atoms/text/Text';
|
|
||||||
import Input from '../../atoms/input/Input';
|
|
||||||
import Button from '../../atoms/button/Button';
|
|
||||||
import ImagePack from '../image-pack/ImagePack';
|
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|
||||||
import { getStateEvent, getStateEvents } from '../../utils/room';
|
|
||||||
|
|
||||||
function useRoomPacks(room) {
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
const [, forceUpdate] = useReducer((count) => count + 1, 0);
|
|
||||||
|
|
||||||
const packEvents = getStateEvents(room, 'im.ponies.room_emotes');
|
|
||||||
const unUsablePacks = [];
|
|
||||||
const usablePacks = packEvents.filter((mEvent) => {
|
|
||||||
if (typeof mEvent.getContent()?.images !== 'object') {
|
|
||||||
unUsablePacks.push(mEvent);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleEvent = (event, state, prevEvent) => {
|
|
||||||
if (event.getRoomId() !== room.roomId) return;
|
|
||||||
if (event.getType() !== 'im.ponies.room_emotes') return;
|
|
||||||
if (!prevEvent?.getContent()?.images || !event.getContent().images) {
|
|
||||||
forceUpdate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
mx.on('RoomState.events', handleEvent);
|
|
||||||
return () => {
|
|
||||||
mx.removeListener('RoomState.events', handleEvent);
|
|
||||||
};
|
|
||||||
}, [room, mx]);
|
|
||||||
|
|
||||||
const isStateKeyAvailable = (key) => !getStateEvent(room, 'im.ponies.room_emotes', key);
|
|
||||||
|
|
||||||
const createPack = async (name) => {
|
|
||||||
const packContent = {
|
|
||||||
pack: { display_name: name },
|
|
||||||
images: {},
|
|
||||||
};
|
|
||||||
let stateKey = '';
|
|
||||||
if (unUsablePacks.length > 0) {
|
|
||||||
const mEvent = unUsablePacks[0];
|
|
||||||
stateKey = mEvent.getStateKey();
|
|
||||||
} else {
|
|
||||||
stateKey = packContent.pack.display_name.replace(/\s/g, '-');
|
|
||||||
if (!isStateKeyAvailable(stateKey)) {
|
|
||||||
stateKey = suffixRename(
|
|
||||||
stateKey,
|
|
||||||
isStateKeyAvailable,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await mx.sendStateEvent(room.roomId, 'im.ponies.room_emotes', packContent, stateKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deletePack = async (stateKey) => {
|
|
||||||
await mx.sendStateEvent(room.roomId, 'im.ponies.room_emotes', {}, stateKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
usablePacks,
|
|
||||||
createPack,
|
|
||||||
deletePack,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function RoomEmojis({ roomId }) {
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
const room = mx.getRoom(roomId);
|
|
||||||
|
|
||||||
const { usablePacks, createPack, deletePack } = useRoomPacks(room);
|
|
||||||
|
|
||||||
const canChange = room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.maySendStateEvent('im.ponies.room_emote', mx.getUserId());
|
|
||||||
|
|
||||||
const handlePackCreate = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const { nameInput } = e.target;
|
|
||||||
const name = nameInput.value.trim();
|
|
||||||
if (name === '') return;
|
|
||||||
nameInput.value = '';
|
|
||||||
|
|
||||||
createPack(name);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="room-emojis">
|
|
||||||
{ canChange && (
|
|
||||||
<div className="room-emojis__add-pack">
|
|
||||||
<MenuHeader>Create Pack</MenuHeader>
|
|
||||||
<form onSubmit={handlePackCreate}>
|
|
||||||
<Input name="nameInput" placeholder="Pack Name" required />
|
|
||||||
<Button variant="primary" type="submit">Create pack</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{
|
|
||||||
usablePacks.length > 0
|
|
||||||
? usablePacks.reverse().map((mEvent) => (
|
|
||||||
<ImagePack
|
|
||||||
key={mEvent.getId()}
|
|
||||||
roomId={roomId}
|
|
||||||
stateKey={mEvent.getStateKey()}
|
|
||||||
handlePackDelete={canChange ? deletePack : undefined}
|
|
||||||
/>
|
|
||||||
)) : (
|
|
||||||
<div className="room-emojis__empty">
|
|
||||||
<Text>No emoji or sticker pack.</Text>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
RoomEmojis.propTypes = {
|
|
||||||
roomId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RoomEmojis;
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
.room-emojis {
|
|
||||||
.image-pack,
|
|
||||||
.room-emojis__add-pack,
|
|
||||||
.room-emojis__empty {
|
|
||||||
margin: var(--sp-normal) 0;
|
|
||||||
background-color: var(--bg-surface);
|
|
||||||
border-radius: var(--bo-radius);
|
|
||||||
box-shadow: var(--bs-surface-border);
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
& > .context-menu__header:first-child {
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&__add-pack {
|
|
||||||
& form {
|
|
||||||
margin: var(--sp-normal);
|
|
||||||
display: flex;
|
|
||||||
gap: var(--sp-normal);
|
|
||||||
& .input-container {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&__empty {
|
|
||||||
padding: var(--sp-extra-loose) var(--sp-normal);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
import React, { useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './RoomEncryption.scss';
|
|
||||||
import { EventTimeline } from 'matrix-js-sdk';
|
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
|
||||||
import Toggle from '../../atoms/button/Toggle';
|
|
||||||
import SettingTile from '../setting-tile/SettingTile';
|
|
||||||
|
|
||||||
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|
||||||
import { getStateEvents } from '../../utils/room';
|
|
||||||
|
|
||||||
function RoomEncryption({ roomId }) {
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
const room = mx.getRoom(roomId);
|
|
||||||
const encryptionEvents = getStateEvents(room, 'm.room.encryption');
|
|
||||||
const [isEncrypted, setIsEncrypted] = useState(encryptionEvents.length > 0);
|
|
||||||
const canEnableEncryption = room.getLiveTimeline().getState(EventTimeline.FORWARDS).maySendStateEvent('m.room.encryption', mx.getUserId());
|
|
||||||
|
|
||||||
const handleEncryptionEnable = async () => {
|
|
||||||
const joinRule = room.getJoinRule();
|
|
||||||
const confirmMsg1 = 'It is not recommended to add encryption in public room. Anyone can find and join public rooms, so anyone can read messages in them.';
|
|
||||||
const confirmMsg2 = 'Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly';
|
|
||||||
|
|
||||||
const isConfirmed1 = (joinRule === 'public')
|
|
||||||
? await confirmDialog('Enable encryption', confirmMsg1, 'Continue', 'caution')
|
|
||||||
: true;
|
|
||||||
if (!isConfirmed1) return;
|
|
||||||
if (await confirmDialog('Enable encryption', confirmMsg2, 'Enable', 'caution')) {
|
|
||||||
setIsEncrypted(true);
|
|
||||||
mx.sendStateEvent(roomId, 'm.room.encryption', {
|
|
||||||
algorithm: 'm.megolm.v1.aes-sha2',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="room-encryption">
|
|
||||||
<SettingTile
|
|
||||||
title="Enable room encryption"
|
|
||||||
content={(
|
|
||||||
<Text variant="b3">Once enabled, encryption cannot be disabled.</Text>
|
|
||||||
)}
|
|
||||||
options={(
|
|
||||||
<Toggle
|
|
||||||
isActive={isEncrypted}
|
|
||||||
onToggle={handleEncryptionEnable}
|
|
||||||
disabled={isEncrypted || !canEnableEncryption}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
RoomEncryption.propTypes = {
|
|
||||||
roomId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RoomEncryption;
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
.room-encryption {
|
|
||||||
& .setting-tile {
|
|
||||||
margin: var(--sp-normal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './RoomHistoryVisibility.scss';
|
|
||||||
|
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
|
||||||
import RadioButton from '../../atoms/button/RadioButton';
|
|
||||||
import { MenuItem } from '../../atoms/context-menu/ContextMenu';
|
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|
||||||
|
|
||||||
const visibility = {
|
|
||||||
WORLD_READABLE: 'world_readable',
|
|
||||||
SHARED: 'shared',
|
|
||||||
INVITED: 'invited',
|
|
||||||
JOINED: 'joined',
|
|
||||||
};
|
|
||||||
|
|
||||||
const items = [{
|
|
||||||
iconSrc: null,
|
|
||||||
text: 'Anyone (including guests)',
|
|
||||||
type: visibility.WORLD_READABLE,
|
|
||||||
}, {
|
|
||||||
iconSrc: null,
|
|
||||||
text: 'Members (all messages)',
|
|
||||||
type: visibility.SHARED,
|
|
||||||
}, {
|
|
||||||
iconSrc: null,
|
|
||||||
text: 'Members (messages after invite)',
|
|
||||||
type: visibility.INVITED,
|
|
||||||
}, {
|
|
||||||
iconSrc: null,
|
|
||||||
text: 'Members (messages after join)',
|
|
||||||
type: visibility.JOINED,
|
|
||||||
}];
|
|
||||||
|
|
||||||
|
|
||||||
function useVisibility(roomId) {
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
const room = mx.getRoom(roomId);
|
|
||||||
|
|
||||||
const [activeType, setActiveType] = useState(room.getHistoryVisibility());
|
|
||||||
useEffect(() => {
|
|
||||||
setActiveType(room.getHistoryVisibility());
|
|
||||||
}, [room]);
|
|
||||||
|
|
||||||
const setVisibility = useCallback((item) => {
|
|
||||||
if (item.type === activeType.type) return;
|
|
||||||
setActiveType(item.type);
|
|
||||||
mx.sendStateEvent(
|
|
||||||
roomId, 'm.room.history_visibility',
|
|
||||||
{
|
|
||||||
history_visibility: item.type,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}, [mx, activeType, roomId]);
|
|
||||||
|
|
||||||
return [activeType, setVisibility];
|
|
||||||
}
|
|
||||||
|
|
||||||
function RoomHistoryVisibility({ roomId }) {
|
|
||||||
const [activeType, setVisibility] = useVisibility(roomId);
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
const userId = mx.getUserId();
|
|
||||||
const room = mx.getRoom(roomId);
|
|
||||||
const { currentState } = room;
|
|
||||||
|
|
||||||
const canChange = currentState.maySendStateEvent('m.room.history_visibility', userId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="room-history-visibility">
|
|
||||||
{
|
|
||||||
items.map((item) => (
|
|
||||||
<MenuItem
|
|
||||||
variant={activeType === item.type ? 'positive' : 'surface'}
|
|
||||||
key={item.type}
|
|
||||||
iconSrc={item.iconSrc}
|
|
||||||
onClick={() => setVisibility(item)}
|
|
||||||
disabled={(!canChange)}
|
|
||||||
>
|
|
||||||
<Text varient="b1">
|
|
||||||
<span>{item.text}</span>
|
|
||||||
<RadioButton isActive={activeType === item.type} />
|
|
||||||
</Text>
|
|
||||||
</MenuItem>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
<Text variant="b3">Changes to history visibility will only apply to future messages. The visibility of existing history will have no effect.</Text>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
RoomHistoryVisibility.propTypes = {
|
|
||||||
roomId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RoomHistoryVisibility;
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
@use '../../partials/flex';
|
|
||||||
@use '../../partials/dir';
|
|
||||||
@use '../../partials/text';
|
|
||||||
|
|
||||||
.room-history-visibility {
|
|
||||||
& .context-menu__item .text {
|
|
||||||
margin: 0 !important;
|
|
||||||
@extend .cp-fx__item-one;
|
|
||||||
@extend .cp-fx__row--s-c;
|
|
||||||
|
|
||||||
& span:first-child {
|
|
||||||
@extend .cp-fx__item-one;
|
|
||||||
@extend .cp-txt__ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .radio-btn {
|
|
||||||
@include dir.side(margin, var(--sp-tight), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .text {
|
|
||||||
margin: var(--sp-normal);
|
|
||||||
margin-top: var(--sp-ultra-tight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './RoomIntro.scss';
|
|
||||||
|
|
||||||
import colorMXID from '../../../util/colorMXID';
|
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
|
||||||
import Avatar from '../../atoms/avatar/Avatar';
|
|
||||||
|
|
||||||
function RoomIntro({
|
|
||||||
roomId, avatarSrc, name, heading, desc, time,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="room-intro">
|
|
||||||
<Avatar imageSrc={avatarSrc} text={name} bgColor={colorMXID(roomId)} size="large" />
|
|
||||||
<div className="room-intro__content">
|
|
||||||
<Text className="room-intro__name" variant="h1" weight="medium" primary>{heading}</Text>
|
|
||||||
<Text className="room-intro__desc" variant="b1">{desc}</Text>
|
|
||||||
{ time !== null && <Text className="room-intro__time" variant="b3">{time}</Text>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
RoomIntro.defaultProps = {
|
|
||||||
avatarSrc: null,
|
|
||||||
time: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
RoomIntro.propTypes = {
|
|
||||||
roomId: PropTypes.string.isRequired,
|
|
||||||
avatarSrc: PropTypes.oneOfType([
|
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.bool,
|
|
||||||
]),
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
heading: PropTypes.node.isRequired,
|
|
||||||
desc: PropTypes.node.isRequired,
|
|
||||||
time: PropTypes.node,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RoomIntro;
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
@use '../../partials/dir';
|
|
||||||
|
|
||||||
.room-intro {
|
|
||||||
margin-top: calc(2 * var(--sp-extra-loose));
|
|
||||||
margin-bottom: var(--sp-extra-loose);
|
|
||||||
--left-pad: calc(var(--sp-normal) + var(--av-small) + var(--sp-tight));
|
|
||||||
@include dir.side(padding, var(--left-pad), var(--sp-extra-tight));
|
|
||||||
|
|
||||||
.room-intro__content {
|
|
||||||
margin-top: var(--sp-extra-loose);
|
|
||||||
width: calc(100% - 88px);
|
|
||||||
}
|
|
||||||
&__name {
|
|
||||||
color: var(--tc-surface-high);
|
|
||||||
}
|
|
||||||
&__desc {
|
|
||||||
color: var(--tc-surface-normal);
|
|
||||||
margin: var(--sp-tight) 0 var(--sp-extra-tight);
|
|
||||||
& a {
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&__time {
|
|
||||||
color: var(--tc-surface-low);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,185 +0,0 @@
|
||||||
import React, {
|
|
||||||
useState, useEffect, useCallback,
|
|
||||||
} from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './RoomMembers.scss';
|
|
||||||
|
|
||||||
import colorMXID from '../../../util/colorMXID';
|
|
||||||
import { openProfileViewer } from '../../../client/action/navigation';
|
|
||||||
import { getUsernameOfRoomMember, getPowerLabel } from '../../../util/matrixUtil';
|
|
||||||
import AsyncSearch from '../../../util/AsyncSearch';
|
|
||||||
import { memberByAtoZ, memberByPowerLevel } from '../../../util/sort';
|
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
|
||||||
import Button from '../../atoms/button/Button';
|
|
||||||
import Input from '../../atoms/input/Input';
|
|
||||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
|
||||||
import SegmentedControls from '../../atoms/segmented-controls/SegmentedControls';
|
|
||||||
import PeopleSelector from '../people-selector/PeopleSelector';
|
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|
||||||
|
|
||||||
const PER_PAGE_MEMBER = 50;
|
|
||||||
|
|
||||||
function normalizeMembers(mx, members) {
|
|
||||||
return members.map((member) => ({
|
|
||||||
userId: member.userId,
|
|
||||||
name: getUsernameOfRoomMember(member),
|
|
||||||
username: member.userId.slice(1, member.userId.indexOf(':')),
|
|
||||||
avatarSrc: member.getAvatarUrl(mx.baseUrl, 24, 24, 'crop'),
|
|
||||||
peopleRole: getPowerLabel(member.powerLevel),
|
|
||||||
powerLevel: members.powerLevel,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function useMemberOfMembership(roomId, membership) {
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
const room = mx.getRoom(roomId);
|
|
||||||
const [members, setMembers] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let isMounted = true;
|
|
||||||
let isLoadingMembers = false;
|
|
||||||
|
|
||||||
const updateMemberList = (event) => {
|
|
||||||
if (isLoadingMembers) return;
|
|
||||||
if (event && event?.getRoomId() !== roomId) return;
|
|
||||||
const memberOfMembership = normalizeMembers(
|
|
||||||
mx,
|
|
||||||
room.getMembersWithMembership(membership)
|
|
||||||
.sort(memberByAtoZ).sort(memberByPowerLevel),
|
|
||||||
);
|
|
||||||
setMembers(memberOfMembership);
|
|
||||||
};
|
|
||||||
|
|
||||||
updateMemberList();
|
|
||||||
isLoadingMembers = true;
|
|
||||||
room.loadMembersIfNeeded().then(() => {
|
|
||||||
isLoadingMembers = false;
|
|
||||||
if (!isMounted) return;
|
|
||||||
updateMemberList();
|
|
||||||
});
|
|
||||||
|
|
||||||
mx.on('RoomMember.membership', updateMemberList);
|
|
||||||
mx.on('RoomMember.powerLevel', updateMemberList);
|
|
||||||
return () => {
|
|
||||||
isMounted = false;
|
|
||||||
mx.removeListener('RoomMember.membership', updateMemberList);
|
|
||||||
mx.removeListener('RoomMember.powerLevel', updateMemberList);
|
|
||||||
};
|
|
||||||
}, [mx, membership]);
|
|
||||||
|
|
||||||
return [members];
|
|
||||||
}
|
|
||||||
|
|
||||||
function useSearchMembers(members) {
|
|
||||||
const [searchMembers, setSearchMembers] = useState(null);
|
|
||||||
const [asyncSearch] = useState(new AsyncSearch());
|
|
||||||
|
|
||||||
const reSearch = useCallback(() => {
|
|
||||||
if (searchMembers) {
|
|
||||||
asyncSearch.search(searchMembers.term);
|
|
||||||
}
|
|
||||||
}, [searchMembers, asyncSearch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
asyncSearch.setup(members, {
|
|
||||||
keys: ['name', 'username', 'userId'],
|
|
||||||
limit: PER_PAGE_MEMBER,
|
|
||||||
});
|
|
||||||
reSearch();
|
|
||||||
}, [members, asyncSearch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleSearchData = (data, term) => setSearchMembers({ data, term });
|
|
||||||
asyncSearch.on(asyncSearch.RESULT_SENT, handleSearchData);
|
|
||||||
return () => {
|
|
||||||
asyncSearch.removeListener(asyncSearch.RESULT_SENT, handleSearchData);
|
|
||||||
};
|
|
||||||
}, [asyncSearch]);
|
|
||||||
|
|
||||||
const handleSearch = (e) => {
|
|
||||||
const term = e.target.value;
|
|
||||||
if (term === '' || term === undefined) {
|
|
||||||
setSearchMembers(null);
|
|
||||||
} else asyncSearch.search(term);
|
|
||||||
};
|
|
||||||
|
|
||||||
return [searchMembers, handleSearch];
|
|
||||||
}
|
|
||||||
|
|
||||||
function RoomMembers({ roomId }) {
|
|
||||||
const [itemCount, setItemCount] = useState(PER_PAGE_MEMBER);
|
|
||||||
const [membership, setMembership] = useState('join');
|
|
||||||
const [members] = useMemberOfMembership(roomId, membership);
|
|
||||||
const [searchMembers, handleSearch] = useSearchMembers(members);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setItemCount(PER_PAGE_MEMBER);
|
|
||||||
}, [searchMembers]);
|
|
||||||
|
|
||||||
const loadMorePeople = () => {
|
|
||||||
setItemCount(itemCount + PER_PAGE_MEMBER);
|
|
||||||
};
|
|
||||||
|
|
||||||
const mList = searchMembers ? searchMembers.data : members.slice(0, itemCount);
|
|
||||||
return (
|
|
||||||
<div className="room-members">
|
|
||||||
<MenuHeader>Search member</MenuHeader>
|
|
||||||
<Input
|
|
||||||
onChange={handleSearch}
|
|
||||||
placeholder="Search for name"
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<div className="room-members__header">
|
|
||||||
<MenuHeader>{`${searchMembers ? `Found — ${mList.length}` : members.length} members`}</MenuHeader>
|
|
||||||
<SegmentedControls
|
|
||||||
selected={
|
|
||||||
(() => {
|
|
||||||
const getSegmentIndex = { join: 0, invite: 1, ban: 2 };
|
|
||||||
return getSegmentIndex[membership];
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
segments={[{ text: 'Joined' }, { text: 'Invited' }, { text: 'Banned' }]}
|
|
||||||
onSelect={(index) => {
|
|
||||||
const memberships = ['join', 'invite', 'ban'];
|
|
||||||
setMembership(memberships[index]);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="room-members__list">
|
|
||||||
{mList.map((member) => (
|
|
||||||
<PeopleSelector
|
|
||||||
key={member.userId}
|
|
||||||
onClick={() => openProfileViewer(member.userId, roomId)}
|
|
||||||
avatarSrc={member.avatarSrc}
|
|
||||||
name={member.name}
|
|
||||||
color={colorMXID(member.userId)}
|
|
||||||
peopleRole={member.peopleRole}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{
|
|
||||||
(searchMembers?.data.length === 0 || members.length === 0)
|
|
||||||
&& (
|
|
||||||
<div className="room-members__status">
|
|
||||||
<Text variant="b2">
|
|
||||||
{searchMembers ? `No results found for "${searchMembers.term}"` : 'No members to display'}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
mList.length !== 0
|
|
||||||
&& members.length > itemCount
|
|
||||||
&& searchMembers === null
|
|
||||||
&& <Button onClick={loadMorePeople}>View more</Button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
RoomMembers.propTypes = {
|
|
||||||
roomId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RoomMembers;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue