Remove old settings components (#2296)
This commit is contained in:
parent
82805dcfdd
commit
12ae94cd60
37 changed files with 6 additions and 3476 deletions
src
app
molecules
image-pack
ImagePack.jsxImagePack.scssImagePackItem.jsxImagePackItem.scssImagePackProfile.jsxImagePackProfile.scssImagePackUpload.jsxImagePackUpload.scssImagePackUsageSelector.jsx
room-aliases
room-emojis
room-encryption
room-history-visibility
room-intro
room-members
room-notification
room-permissions
room-profile
room-visibility
organisms
client
|
@ -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;
|
|
@ -1,39 +0,0 @@
|
|||
@use '../../partials/flex';
|
||||
@use '../../partials/dir';
|
||||
|
||||
.room-members {
|
||||
& .input-container {
|
||||
margin: var(--sp-normal);
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
& .context-menu__header {
|
||||
@extend .cp-fx__item-one;
|
||||
margin-top: 14px;
|
||||
border-top: 1px solid var(--bg-surface-border);
|
||||
border-bottom: none;
|
||||
}
|
||||
& .segmented-controls {
|
||||
@include dir.side(margin, 0, var(--sp-normal));
|
||||
& > button {
|
||||
padding: var(--sp-ultra-tight) 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__list {
|
||||
|
||||
& .people-selector__container:last-child {
|
||||
margin-bottom: var(--sp-extra-tight);
|
||||
}
|
||||
& > .btn-surface {
|
||||
width: calc(100% - 32px);
|
||||
margin: var(--sp-normal);
|
||||
}
|
||||
}
|
||||
|
||||
&__status {
|
||||
margin: var(--sp-normal);
|
||||
}
|
||||
}
|
|
@ -1,153 +0,0 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomNotification.scss';
|
||||
|
||||
import cons from '../../../client/state/cons';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import RadioButton from '../../atoms/button/RadioButton';
|
||||
import { MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||
|
||||
import BellIC from '../../../../public/res/ic/outlined/bell.svg';
|
||||
import BellRingIC from '../../../../public/res/ic/outlined/bell-ring.svg';
|
||||
import BellPingIC from '../../../../public/res/ic/outlined/bell-ping.svg';
|
||||
import BellOffIC from '../../../../public/res/ic/outlined/bell-off.svg';
|
||||
import { getNotificationType } from '../../utils/room';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
|
||||
const items = [
|
||||
{
|
||||
iconSrc: BellIC,
|
||||
text: 'Global',
|
||||
type: cons.notifs.DEFAULT,
|
||||
},
|
||||
{
|
||||
iconSrc: BellRingIC,
|
||||
text: 'All messages',
|
||||
type: cons.notifs.ALL_MESSAGES,
|
||||
},
|
||||
{
|
||||
iconSrc: BellPingIC,
|
||||
text: 'Mentions & Keywords',
|
||||
type: cons.notifs.MENTIONS_AND_KEYWORDS,
|
||||
},
|
||||
{
|
||||
iconSrc: BellOffIC,
|
||||
text: 'Mute',
|
||||
type: cons.notifs.MUTE,
|
||||
},
|
||||
];
|
||||
|
||||
function setRoomNotifType(mx, roomId, newType) {
|
||||
let roomPushRule;
|
||||
try {
|
||||
roomPushRule = mx.getRoomPushRule('global', roomId);
|
||||
} catch {
|
||||
roomPushRule = undefined;
|
||||
}
|
||||
const promises = [];
|
||||
|
||||
if (newType === cons.notifs.MUTE) {
|
||||
if (roomPushRule) {
|
||||
promises.push(mx.deletePushRule('global', 'room', roomPushRule.rule_id));
|
||||
}
|
||||
promises.push(
|
||||
mx.addPushRule('global', 'override', roomId, {
|
||||
conditions: [
|
||||
{
|
||||
kind: 'event_match',
|
||||
key: 'room_id',
|
||||
pattern: roomId,
|
||||
},
|
||||
],
|
||||
actions: ['dont_notify'],
|
||||
})
|
||||
);
|
||||
return promises;
|
||||
}
|
||||
|
||||
const oldState = getNotificationType(mx, roomId);
|
||||
if (oldState === cons.notifs.MUTE) {
|
||||
promises.push(mx.deletePushRule('global', 'override', roomId));
|
||||
}
|
||||
|
||||
if (newType === cons.notifs.DEFAULT) {
|
||||
if (roomPushRule) {
|
||||
promises.push(mx.deletePushRule('global', 'room', roomPushRule.rule_id));
|
||||
}
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
if (newType === cons.notifs.MENTIONS_AND_KEYWORDS) {
|
||||
promises.push(
|
||||
mx.addPushRule('global', 'room', roomId, {
|
||||
actions: ['dont_notify'],
|
||||
})
|
||||
);
|
||||
promises.push(mx.setPushRuleEnabled('global', 'room', roomId, true));
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
// cons.notifs.ALL_MESSAGES
|
||||
promises.push(
|
||||
mx.addPushRule('global', 'room', roomId, {
|
||||
actions: [
|
||||
'notify',
|
||||
{
|
||||
set_tweak: 'sound',
|
||||
value: 'default',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
promises.push(mx.setPushRuleEnabled('global', 'room', roomId, true));
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
function useNotifications(roomId) {
|
||||
const mx = useMatrixClient();
|
||||
const [activeType, setActiveType] = useState(getNotificationType(mx, roomId));
|
||||
useEffect(() => {
|
||||
setActiveType(getNotificationType(mx, roomId));
|
||||
}, [mx, roomId]);
|
||||
|
||||
const setNotification = useCallback(
|
||||
(item) => {
|
||||
if (item.type === activeType.type) return;
|
||||
setActiveType(item.type);
|
||||
setRoomNotifType(mx, roomId, item.type);
|
||||
},
|
||||
[mx, activeType, roomId]
|
||||
);
|
||||
return [activeType, setNotification];
|
||||
}
|
||||
|
||||
function RoomNotification({ roomId }) {
|
||||
const [activeType, setNotification] = useNotifications(roomId);
|
||||
|
||||
return (
|
||||
<div className="room-notification">
|
||||
{items.map((item) => (
|
||||
<MenuItem
|
||||
variant={activeType === item.type ? 'positive' : 'surface'}
|
||||
key={item.type}
|
||||
iconSrc={item.iconSrc}
|
||||
onClick={() => setNotification(item)}
|
||||
>
|
||||
<Text varient="b1">
|
||||
<span>{item.text}</span>
|
||||
<RadioButton isActive={activeType === item.type} />
|
||||
</Text>
|
||||
</MenuItem>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RoomNotification.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RoomNotification;
|
|
@ -1,19 +0,0 @@
|
|||
@use '../../partials/flex';
|
||||
@use '../../partials/dir';
|
||||
@use '../../partials/text';
|
||||
|
||||
.room-notification {
|
||||
& .context-menu__item .text {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,284 +0,0 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomPermissions.scss';
|
||||
import { EventTimeline } from 'matrix-js-sdk';
|
||||
|
||||
import { getPowerLabel } from '../../../util/matrixUtil';
|
||||
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
import PowerLevelSelector from '../power-level-selector/PowerLevelSelector';
|
||||
import SettingTile from '../setting-tile/SettingTile';
|
||||
|
||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||
|
||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { getStateEvent } from '../../utils/room';
|
||||
|
||||
const permissionsInfo = {
|
||||
users_default: {
|
||||
name: 'Default role',
|
||||
description: 'Set default role for all members.',
|
||||
default: 0,
|
||||
},
|
||||
events_default: {
|
||||
name: 'Send messages',
|
||||
description: 'Set minimum power level to send messages in room.',
|
||||
default: 0,
|
||||
},
|
||||
'm.reaction': {
|
||||
parent: 'events',
|
||||
name: 'Send reactions',
|
||||
description: 'Set minimum power level to send reactions in room.',
|
||||
default: 0,
|
||||
},
|
||||
redact: {
|
||||
name: 'Delete messages sent by others',
|
||||
description: 'Set minimum power level to delete messages in room.',
|
||||
default: 50,
|
||||
},
|
||||
notifications: {
|
||||
name: 'Ping room',
|
||||
description: 'Set minimum power level to ping room.',
|
||||
default: {
|
||||
room: 50,
|
||||
},
|
||||
},
|
||||
'm.space.child': {
|
||||
parent: 'events',
|
||||
name: 'Manage rooms in space',
|
||||
description: 'Set minimum power level to manage rooms in space.',
|
||||
default: 50,
|
||||
},
|
||||
invite: {
|
||||
name: 'Invite',
|
||||
description: 'Set minimum power level to invite members.',
|
||||
default: 50,
|
||||
},
|
||||
kick: {
|
||||
name: 'Kick',
|
||||
description: 'Set minimum power level to kick members.',
|
||||
default: 50,
|
||||
},
|
||||
ban: {
|
||||
name: 'Ban',
|
||||
description: 'Set minimum power level to ban members.',
|
||||
default: 50,
|
||||
},
|
||||
'm.room.avatar': {
|
||||
parent: 'events',
|
||||
name: 'Change avatar',
|
||||
description: 'Set minimum power level to change room/space avatar.',
|
||||
default: 50,
|
||||
},
|
||||
'm.room.name': {
|
||||
parent: 'events',
|
||||
name: 'Change name',
|
||||
description: 'Set minimum power level to change room/space name.',
|
||||
default: 50,
|
||||
},
|
||||
'm.room.topic': {
|
||||
parent: 'events',
|
||||
name: 'Change topic',
|
||||
description: 'Set minimum power level to change room/space topic.',
|
||||
default: 50,
|
||||
},
|
||||
state_default: {
|
||||
name: 'Change settings',
|
||||
description: 'Set minimum power level to change settings.',
|
||||
default: 50,
|
||||
},
|
||||
'm.room.canonical_alias': {
|
||||
parent: 'events',
|
||||
name: 'Change published address',
|
||||
description: 'Set minimum power level to publish and set main address.',
|
||||
default: 50,
|
||||
},
|
||||
'm.room.power_levels': {
|
||||
parent: 'events',
|
||||
name: 'Change permissions',
|
||||
description: 'Set minimum power level to change permissions.',
|
||||
default: 50,
|
||||
},
|
||||
'm.room.encryption': {
|
||||
parent: 'events',
|
||||
name: 'Enable room encryption',
|
||||
description: 'Set minimum power level to enable room encryption.',
|
||||
default: 50,
|
||||
},
|
||||
'm.room.history_visibility': {
|
||||
parent: 'events',
|
||||
name: 'Change history visibility',
|
||||
description: 'Set minimum power level to change room messages history visibility.',
|
||||
default: 50,
|
||||
},
|
||||
'm.room.tombstone': {
|
||||
parent: 'events',
|
||||
name: 'Upgrade room',
|
||||
description: 'Set minimum power level to upgrade room.',
|
||||
default: 50,
|
||||
},
|
||||
'm.room.pinned_events': {
|
||||
parent: 'events',
|
||||
name: 'Pin messages',
|
||||
description: 'Set minimum power level to pin messages in room.',
|
||||
default: 50,
|
||||
},
|
||||
'm.room.server_acl': {
|
||||
parent: 'events',
|
||||
name: 'Change server ACLs',
|
||||
description: 'Set minimum power level to change server ACLs.',
|
||||
default: 50,
|
||||
},
|
||||
'im.vector.modular.widgets': {
|
||||
parent: 'events',
|
||||
name: 'Modify widgets',
|
||||
description: 'Set minimum power level to modify room widgets.',
|
||||
default: 50,
|
||||
},
|
||||
};
|
||||
|
||||
const roomPermsGroups = {
|
||||
'General Permissions': ['users_default', 'events_default', 'm.reaction', 'redact', 'notifications'],
|
||||
'Manage members permissions': ['invite', 'kick', 'ban'],
|
||||
'Room profile permissions': ['m.room.avatar', 'm.room.name', 'm.room.topic'],
|
||||
'Settings permissions': ['state_default', 'm.room.canonical_alias', 'm.room.power_levels', 'm.room.encryption', 'm.room.history_visibility'],
|
||||
'Other permissions': ['m.room.tombstone', 'm.room.pinned_events', 'm.room.server_acl', 'im.vector.modular.widgets'],
|
||||
};
|
||||
|
||||
const spacePermsGroups = {
|
||||
'General Permissions': ['users_default', 'm.space.child'],
|
||||
'Manage members permissions': ['invite', 'kick', 'ban'],
|
||||
'Space profile permissions': ['m.room.avatar', 'm.room.name', 'm.room.topic'],
|
||||
'Settings permissions': ['state_default', 'm.room.canonical_alias', 'm.room.power_levels'],
|
||||
};
|
||||
|
||||
function useRoomStateUpdate(roomId) {
|
||||
const [, forceUpdate] = useForceUpdate();
|
||||
const mx = useMatrixClient();
|
||||
|
||||
useEffect(() => {
|
||||
const handleStateEvent = (event) => {
|
||||
if (event.getRoomId() !== roomId) return;
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
mx.on('RoomState.events', handleStateEvent);
|
||||
return () => {
|
||||
mx.removeListener('RoomState.events', handleStateEvent);
|
||||
};
|
||||
}, [mx, roomId]);
|
||||
}
|
||||
|
||||
function RoomPermissions({ roomId }) {
|
||||
useRoomStateUpdate(roomId);
|
||||
const mx = useMatrixClient();
|
||||
const room = mx.getRoom(roomId);
|
||||
const pLEvent = getStateEvent(room, 'm.room.power_levels');
|
||||
const permissions = pLEvent.getContent();
|
||||
const canChangePermission = room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.maySendStateEvent('m.room.power_levels', mx.getUserId());
|
||||
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel ?? 100;
|
||||
|
||||
const handlePowerSelector = (e, permKey, parentKey, powerLevel) => {
|
||||
const handlePowerLevelChange = (newPowerLevel) => {
|
||||
if (powerLevel === newPowerLevel) return;
|
||||
|
||||
const newPermissions = { ...permissions };
|
||||
if (parentKey) {
|
||||
newPermissions[parentKey] = {
|
||||
...permissions[parentKey],
|
||||
[permKey]: newPowerLevel,
|
||||
};
|
||||
} else if (permKey === 'notifications') {
|
||||
newPermissions[permKey] = {
|
||||
...permissions[permKey],
|
||||
room: newPowerLevel,
|
||||
};
|
||||
} else {
|
||||
newPermissions[permKey] = newPowerLevel;
|
||||
}
|
||||
|
||||
mx.sendStateEvent(roomId, 'm.room.power_levels', newPermissions);
|
||||
};
|
||||
|
||||
openReusableContextMenu(
|
||||
'bottom',
|
||||
getEventCords(e, '.btn-surface'),
|
||||
(closeMenu) => (
|
||||
<PowerLevelSelector
|
||||
value={powerLevel}
|
||||
max={myPowerLevel}
|
||||
onSelect={(pl) => {
|
||||
closeMenu();
|
||||
handlePowerLevelChange(pl);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const permsGroups = room.isSpaceRoom() ? spacePermsGroups : roomPermsGroups;
|
||||
return (
|
||||
<div className="room-permissions">
|
||||
{
|
||||
Object.keys(permsGroups).map((groupKey) => {
|
||||
const groupedPermKeys = permsGroups[groupKey];
|
||||
return (
|
||||
<div className="room-permissions__card" key={groupKey}>
|
||||
<MenuHeader>{groupKey}</MenuHeader>
|
||||
{
|
||||
groupedPermKeys.map((permKey) => {
|
||||
const permInfo = permissionsInfo[permKey];
|
||||
|
||||
let powerLevel = 0;
|
||||
let permValue = permInfo.parent
|
||||
? permissions[permInfo.parent]?.[permKey]
|
||||
: permissions[permKey];
|
||||
|
||||
if (permValue === undefined) permValue = permInfo.default;
|
||||
|
||||
if (typeof permValue === 'number') {
|
||||
powerLevel = permValue;
|
||||
} else if (permKey === 'notifications') {
|
||||
powerLevel = permValue.room ?? 50;
|
||||
}
|
||||
return (
|
||||
<SettingTile
|
||||
key={permKey}
|
||||
title={permInfo.name}
|
||||
content={<Text variant="b3">{permInfo.description}</Text>}
|
||||
options={(
|
||||
<Button
|
||||
onClick={
|
||||
canChangePermission
|
||||
? (e) => handlePowerSelector(e, permKey, permInfo.parent, powerLevel)
|
||||
: null
|
||||
}
|
||||
iconSrc={canChangePermission ? ChevronBottomIC : null}
|
||||
>
|
||||
<Text variant="b2">
|
||||
{`${getPowerLabel(powerLevel) || 'Member'} - ${powerLevel}`}
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RoomPermissions.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RoomPermissions;
|
|
@ -1,11 +0,0 @@
|
|||
.room-permissions {
|
||||
& .setting-tile {
|
||||
margin: 0 var(--sp-normal);
|
||||
margin-top: var(--sp-tight);
|
||||
padding-bottom: var(--sp-tight);
|
||||
border-bottom: 1px solid var(--bg-surface-border);
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,237 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import Linkify from 'linkify-react';
|
||||
import './RoomProfile.scss';
|
||||
|
||||
import cons from '../../../client/state/cons';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import ImageUpload from '../image-upload/ImageUpload';
|
||||
|
||||
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||
import { mDirectAtom } from '../../state/mDirectList';
|
||||
import { LINKIFY_OPTS } from '../../plugins/react-custom-html-parser';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
|
||||
function RoomProfile({ roomId }) {
|
||||
const isMountStore = useStore();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [, forceUpdate] = useForceUpdate();
|
||||
const [status, setStatus] = useState({
|
||||
msg: null,
|
||||
type: cons.status.PRE_FLIGHT,
|
||||
});
|
||||
|
||||
const mx = useMatrixClient();
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
const isDM = mDirects.has(roomId);
|
||||
let avatarSrc = mx.getRoom(roomId).getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
|
||||
avatarSrc = isDM
|
||||
? mx.getRoom(roomId).getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop')
|
||||
: avatarSrc;
|
||||
const room = mx.getRoom(roomId);
|
||||
const { currentState } = room;
|
||||
const roomName = room.name;
|
||||
const roomTopic = currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
|
||||
|
||||
const userId = mx.getUserId();
|
||||
|
||||
const canChangeAvatar = currentState.maySendStateEvent('m.room.avatar', userId);
|
||||
const canChangeName = currentState.maySendStateEvent('m.room.name', userId);
|
||||
const canChangeTopic = currentState.maySendStateEvent('m.room.topic', userId);
|
||||
|
||||
useEffect(() => {
|
||||
isMountStore.setItem(true);
|
||||
const handleStateEvent = (mEvent) => {
|
||||
if (mEvent.event.room_id !== roomId) return;
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
mx.on('RoomState.events', handleStateEvent);
|
||||
return () => {
|
||||
mx.removeListener('RoomState.events', handleStateEvent);
|
||||
isMountStore.setItem(false);
|
||||
setStatus({
|
||||
msg: null,
|
||||
type: cons.status.PRE_FLIGHT,
|
||||
});
|
||||
setIsEditing(false);
|
||||
};
|
||||
}, [mx, roomId]);
|
||||
|
||||
const handleOnSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const { target } = e;
|
||||
const roomNameInput = target.elements['room-name'];
|
||||
const roomTopicInput = target.elements['room-topic'];
|
||||
|
||||
try {
|
||||
if (canChangeName) {
|
||||
const newName = roomNameInput.value;
|
||||
if (newName !== roomName && roomName.trim() !== '') {
|
||||
setStatus({
|
||||
msg: 'Saving room name...',
|
||||
type: cons.status.IN_FLIGHT,
|
||||
});
|
||||
await mx.setRoomName(roomId, newName);
|
||||
}
|
||||
}
|
||||
if (canChangeTopic) {
|
||||
const newTopic = roomTopicInput.value;
|
||||
if (newTopic !== roomTopic) {
|
||||
if (isMountStore.getItem()) {
|
||||
setStatus({
|
||||
msg: 'Saving room topic...',
|
||||
type: cons.status.IN_FLIGHT,
|
||||
});
|
||||
}
|
||||
await mx.setRoomTopic(roomId, newTopic);
|
||||
}
|
||||
}
|
||||
if (!isMountStore.getItem()) return;
|
||||
setStatus({
|
||||
msg: 'Saved successfully',
|
||||
type: cons.status.SUCCESS,
|
||||
});
|
||||
} catch (err) {
|
||||
if (!isMountStore.getItem()) return;
|
||||
setStatus({
|
||||
msg: err.message || 'Unable to save.',
|
||||
type: cons.status.ERROR,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelEditing = () => {
|
||||
setStatus({
|
||||
msg: null,
|
||||
type: cons.status.PRE_FLIGHT,
|
||||
});
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const handleAvatarUpload = async (url) => {
|
||||
if (url === null) {
|
||||
const isConfirmed = await confirmDialog(
|
||||
'Remove avatar',
|
||||
'Are you sure that you want to remove room avatar?',
|
||||
'Remove',
|
||||
'caution'
|
||||
);
|
||||
if (isConfirmed) {
|
||||
await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
|
||||
}
|
||||
} else await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
|
||||
};
|
||||
|
||||
const renderEditNameAndTopic = () => (
|
||||
<form className="room-profile__edit-form" onSubmit={handleOnSubmit}>
|
||||
{canChangeName && (
|
||||
<Input
|
||||
value={roomName}
|
||||
name="room-name"
|
||||
disabled={status.type === cons.status.IN_FLIGHT}
|
||||
label="Name"
|
||||
/>
|
||||
)}
|
||||
{canChangeTopic && (
|
||||
<Input
|
||||
value={roomTopic}
|
||||
name="room-topic"
|
||||
disabled={status.type === cons.status.IN_FLIGHT}
|
||||
minHeight={100}
|
||||
resizable
|
||||
label="Topic"
|
||||
/>
|
||||
)}
|
||||
{(!canChangeName || !canChangeTopic) && (
|
||||
<Text variant="b3">{`You have permission to change ${
|
||||
room.isSpaceRoom() ? 'space' : 'room'
|
||||
} ${canChangeName ? 'name' : 'topic'} only.`}</Text>
|
||||
)}
|
||||
{status.type === cons.status.IN_FLIGHT && <Text variant="b2">{status.msg}</Text>}
|
||||
{status.type === cons.status.SUCCESS && (
|
||||
<Text style={{ color: 'var(--tc-positive-high)' }} variant="b2">
|
||||
{status.msg}
|
||||
</Text>
|
||||
)}
|
||||
{status.type === cons.status.ERROR && (
|
||||
<Text style={{ color: 'var(--tc-danger-high)' }} variant="b2">
|
||||
{status.msg}
|
||||
</Text>
|
||||
)}
|
||||
{status.type !== cons.status.IN_FLIGHT && (
|
||||
<div>
|
||||
<Button type="submit" variant="primary">
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={handleCancelEditing}>Cancel</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
|
||||
const renderNameAndTopic = () => (
|
||||
<div
|
||||
className="room-profile__display"
|
||||
style={{ marginBottom: avatarSrc && canChangeAvatar ? '24px' : '0' }}
|
||||
>
|
||||
<div>
|
||||
<Text variant="h2" weight="medium" primary>
|
||||
{roomName}
|
||||
</Text>
|
||||
{(canChangeName || canChangeTopic) && (
|
||||
<IconButton
|
||||
src={PencilIC}
|
||||
size="extra-small"
|
||||
tooltip="Edit"
|
||||
onClick={() => setIsEditing(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Text variant="b3">{room.getCanonicalAlias() || room.roomId}</Text>
|
||||
{roomTopic && (
|
||||
<Text variant="b2">
|
||||
<Linkify options={LINKIFY_OPTS}>{roomTopic}</Linkify>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="room-profile">
|
||||
<div className="room-profile__content">
|
||||
{!canChangeAvatar && (
|
||||
<Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="large" />
|
||||
)}
|
||||
{canChangeAvatar && (
|
||||
<ImageUpload
|
||||
text={roomName}
|
||||
bgColor={colorMXID(roomId)}
|
||||
imageSrc={avatarSrc}
|
||||
onUpload={handleAvatarUpload}
|
||||
onRequestRemove={() => handleAvatarUpload(null)}
|
||||
/>
|
||||
)}
|
||||
{!isEditing && renderNameAndTopic()}
|
||||
{isEditing && renderEditNameAndTopic()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RoomProfile.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RoomProfile;
|
|
@ -1,53 +0,0 @@
|
|||
@use '../../partials/flex';
|
||||
@use '../../partials/dir';
|
||||
|
||||
.room-profile {
|
||||
|
||||
&__content {
|
||||
@extend .cp-fx__row;
|
||||
& .avatar-container {
|
||||
min-width: var(--av-large);
|
||||
}
|
||||
}
|
||||
|
||||
&__display {
|
||||
align-self: flex-end;
|
||||
@include dir.side(margin, var(--sp-loose), 0);
|
||||
|
||||
& > div:first-child {
|
||||
@extend .cp-fx__row--s-c;
|
||||
& > .text {
|
||||
@include dir.side(margin, 0, var(--sp-extra-tight));
|
||||
}
|
||||
}
|
||||
|
||||
& > *:not(:first-child) {
|
||||
margin-top: var(--sp-ultra-tight);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
&__edit-form {
|
||||
@extend .cp-fx__item-one;
|
||||
@include dir.side(margin, var(--sp-loose), 0);
|
||||
|
||||
& .input-container {
|
||||
margin-bottom: var(--sp-extra-tight);
|
||||
}
|
||||
|
||||
& > .text {
|
||||
margin-bottom: var(--sp-tight);
|
||||
}
|
||||
|
||||
& > *:last-child {
|
||||
@extend .cp-fx__item-one;
|
||||
@extend .cp-fx__row;
|
||||
margin-top: var(--sp-tight);
|
||||
|
||||
.btn-primary {
|
||||
@include dir.side(margin, 0, var(--sp-tight));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomVisibility.scss';
|
||||
import { EventTimeline } from 'matrix-js-sdk';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import RadioButton from '../../atoms/button/RadioButton';
|
||||
import { MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||
|
||||
import HashIC from '../../../../public/res/ic/outlined/hash.svg';
|
||||
import HashLockIC from '../../../../public/res/ic/outlined/hash-lock.svg';
|
||||
import HashGlobeIC from '../../../../public/res/ic/outlined/hash-globe.svg';
|
||||
import SpaceIC from '../../../../public/res/ic/outlined/space.svg';
|
||||
import SpaceLockIC from '../../../../public/res/ic/outlined/space-lock.svg';
|
||||
import SpaceGlobeIC from '../../../../public/res/ic/outlined/space-globe.svg';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
|
||||
const visibility = {
|
||||
INVITE: 'invite',
|
||||
RESTRICTED: 'restricted',
|
||||
PUBLIC: 'public',
|
||||
};
|
||||
|
||||
function setJoinRule(mx, roomId, type) {
|
||||
let allow;
|
||||
if (type === visibility.RESTRICTED) {
|
||||
const { currentState } = mx.getRoom(roomId);
|
||||
const mEvent = currentState.getStateEvents('m.space.parent')[0];
|
||||
if (!mEvent) return Promise.resolve(undefined);
|
||||
|
||||
allow = [{
|
||||
room_id: mEvent.getStateKey(),
|
||||
type: 'm.room_membership',
|
||||
}];
|
||||
}
|
||||
|
||||
return mx.sendStateEvent(
|
||||
roomId,
|
||||
'm.room.join_rules',
|
||||
{
|
||||
join_rule: type,
|
||||
allow,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function useVisibility(roomId) {
|
||||
const mx = useMatrixClient();
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
const [activeType, setActiveType] = useState(room.getJoinRule());
|
||||
useEffect(() => {
|
||||
setActiveType(room.getJoinRule());
|
||||
}, [room]);
|
||||
|
||||
const setNotification = useCallback((item) => {
|
||||
if (item.type === activeType.type) return;
|
||||
setActiveType(item.type);
|
||||
setJoinRule(mx, roomId, item.type);
|
||||
}, [mx, activeType, roomId]);
|
||||
|
||||
return [activeType, setNotification];
|
||||
}
|
||||
|
||||
function RoomVisibility({ roomId }) {
|
||||
const [activeType, setVisibility] = useVisibility(roomId);
|
||||
const mx = useMatrixClient();
|
||||
const room = mx.getRoom(roomId);
|
||||
const isSpace = room.isSpaceRoom();
|
||||
const { currentState } = room;
|
||||
|
||||
const noSpaceParent = currentState.getStateEvents('m.space.parent').length === 0;
|
||||
const mCreate = currentState.getStateEvents('m.room.create')[0]?.getContent();
|
||||
const roomVersion = Number(mCreate?.room_version ?? 0);
|
||||
|
||||
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
||||
const canChange = room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.hasSufficientPowerLevelFor('state_default', myPowerlevel);
|
||||
|
||||
const items = [{
|
||||
iconSrc: isSpace ? SpaceLockIC : HashLockIC,
|
||||
text: 'Private (invite only)',
|
||||
type: visibility.INVITE,
|
||||
unsupported: false,
|
||||
}, {
|
||||
iconSrc: isSpace ? SpaceIC : HashIC,
|
||||
text: roomVersion < 8 ? 'Restricted (unsupported: required room upgrade)' : 'Restricted (space member can join)',
|
||||
type: visibility.RESTRICTED,
|
||||
unsupported: roomVersion < 8 || noSpaceParent,
|
||||
}, {
|
||||
iconSrc: isSpace ? SpaceGlobeIC : HashGlobeIC,
|
||||
text: 'Public (anyone can join)',
|
||||
type: visibility.PUBLIC,
|
||||
unsupported: false,
|
||||
}];
|
||||
|
||||
return (
|
||||
<div className="room-visibility">
|
||||
{
|
||||
items.map((item) => (
|
||||
<MenuItem
|
||||
variant={activeType === item.type ? 'positive' : 'surface'}
|
||||
key={item.type}
|
||||
iconSrc={item.iconSrc}
|
||||
onClick={() => setVisibility(item)}
|
||||
disabled={(!canChange || item.unsupported)}
|
||||
>
|
||||
<Text varient="b1">
|
||||
<span>{item.text}</span>
|
||||
<RadioButton isActive={activeType === item.type} />
|
||||
</Text>
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RoomVisibility.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RoomVisibility;
|
|
@ -1,19 +0,0 @@
|
|||
@use '../../partials/flex';
|
||||
@use '../../partials/dir';
|
||||
@use '../../partials/text';
|
||||
|
||||
.room-visibility {
|
||||
& .context-menu__item .text {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,8 +4,6 @@ import cons from '../../../client/state/cons';
|
|||
import navigation from '../../../client/state/navigation';
|
||||
|
||||
import InviteUser from '../invite-user/InviteUser';
|
||||
import SpaceSettings from '../space-settings/SpaceSettings';
|
||||
import RoomSettings from '../room/RoomSettings';
|
||||
|
||||
function Windows() {
|
||||
const [inviteUser, changeInviteUser] = useState({
|
||||
|
@ -30,16 +28,12 @@ function Windows() {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<InviteUser
|
||||
isOpen={inviteUser.isOpen}
|
||||
roomId={inviteUser.roomId}
|
||||
searchTerm={inviteUser.searchTerm}
|
||||
onRequestClose={() => changeInviteUser({ isOpen: false, roomId: undefined })}
|
||||
/>
|
||||
<SpaceSettings />
|
||||
<RoomSettings />
|
||||
</>
|
||||
<InviteUser
|
||||
isOpen={inviteUser.isOpen}
|
||||
roomId={inviteUser.roomId}
|
||||
searchTerm={inviteUser.searchTerm}
|
||||
onRequestClose={() => changeInviteUser({ isOpen: false, roomId: undefined })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,200 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomSettings.scss';
|
||||
|
||||
import cons from '../../../client/state/cons';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Tabs from '../../atoms/tabs/Tabs';
|
||||
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||
import RoomProfile from '../../molecules/room-profile/RoomProfile';
|
||||
import RoomNotification from '../../molecules/room-notification/RoomNotification';
|
||||
import RoomVisibility from '../../molecules/room-visibility/RoomVisibility';
|
||||
import RoomAliases from '../../molecules/room-aliases/RoomAliases';
|
||||
import RoomHistoryVisibility from '../../molecules/room-history-visibility/RoomHistoryVisibility';
|
||||
import RoomEncryption from '../../molecules/room-encryption/RoomEncryption';
|
||||
import RoomPermissions from '../../molecules/room-permissions/RoomPermissions';
|
||||
import RoomMembers from '../../molecules/room-members/RoomMembers';
|
||||
import RoomEmojis from '../../molecules/room-emojis/RoomEmojis';
|
||||
|
||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
||||
import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
|
||||
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
|
||||
import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg';
|
||||
import LockIC from '../../../../public/res/ic/outlined/lock.svg';
|
||||
import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
|
||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||
|
||||
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
||||
import PopupWindow from '../../molecules/popup-window/PopupWindow';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
|
||||
const tabText = {
|
||||
GENERAL: 'General',
|
||||
MEMBERS: 'Members',
|
||||
EMOJIS: 'Emojis',
|
||||
PERMISSIONS: 'Permissions',
|
||||
SECURITY: 'Security',
|
||||
};
|
||||
|
||||
const tabItems = [
|
||||
{
|
||||
iconSrc: SettingsIC,
|
||||
text: tabText.GENERAL,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: UserIC,
|
||||
text: tabText.MEMBERS,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: EmojiIC,
|
||||
text: tabText.EMOJIS,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: ShieldUserIC,
|
||||
text: tabText.PERMISSIONS,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: LockIC,
|
||||
text: tabText.SECURITY,
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
function GeneralSettings({ roomId }) {
|
||||
const mx = useMatrixClient();
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="room-settings__card">
|
||||
<MenuHeader>Options</MenuHeader>
|
||||
<MenuItem
|
||||
variant="danger"
|
||||
onClick={async () => {
|
||||
const isConfirmed = await confirmDialog(
|
||||
'Leave room',
|
||||
`Are you sure that you want to leave "${room.name}" room?`,
|
||||
'Leave',
|
||||
'danger'
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
mx.leave(roomId);
|
||||
}}
|
||||
iconSrc={LeaveArrowIC}
|
||||
>
|
||||
Leave
|
||||
</MenuItem>
|
||||
</div>
|
||||
<div className="room-settings__card">
|
||||
<MenuHeader>Notification (Changing this will only affect you)</MenuHeader>
|
||||
<RoomNotification roomId={roomId} />
|
||||
</div>
|
||||
<div className="room-settings__card">
|
||||
<MenuHeader>Room visibility (who can join)</MenuHeader>
|
||||
<RoomVisibility roomId={roomId} />
|
||||
</div>
|
||||
<div className="room-settings__card">
|
||||
<MenuHeader>Room addresses</MenuHeader>
|
||||
<RoomAliases roomId={roomId} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
GeneralSettings.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function SecuritySettings({ roomId }) {
|
||||
return (
|
||||
<>
|
||||
<div className="room-settings__card">
|
||||
<MenuHeader>Encryption</MenuHeader>
|
||||
<RoomEncryption roomId={roomId} />
|
||||
</div>
|
||||
<div className="room-settings__card">
|
||||
<MenuHeader>Message history visibility</MenuHeader>
|
||||
<RoomHistoryVisibility roomId={roomId} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
SecuritySettings.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function useWindowToggle(setSelectedTab) {
|
||||
const [window, setWindow] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const openRoomSettings = (roomId, tab) => {
|
||||
setWindow({ roomId, tabText });
|
||||
const tabItem = tabItems.find((item) => item.text === tab);
|
||||
if (tabItem) setSelectedTab(tabItem);
|
||||
};
|
||||
navigation.on(cons.events.navigation.ROOM_SETTINGS_TOGGLED, openRoomSettings);
|
||||
return () => {
|
||||
navigation.removeListener(cons.events.navigation.ROOM_SETTINGS_TOGGLED, openRoomSettings);
|
||||
};
|
||||
}, [setSelectedTab]);
|
||||
|
||||
const requestClose = () => setWindow(null);
|
||||
|
||||
return [window, requestClose];
|
||||
}
|
||||
|
||||
function RoomSettings() {
|
||||
const [selectedTab, setSelectedTab] = useState(tabItems[0]);
|
||||
const [window, requestClose] = useWindowToggle(setSelectedTab);
|
||||
const isOpen = window !== null;
|
||||
const roomId = window?.roomId;
|
||||
const mx = useMatrixClient();
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
const handleTabChange = (tabItem) => {
|
||||
setSelectedTab(tabItem);
|
||||
};
|
||||
|
||||
return (
|
||||
<PopupWindow
|
||||
isOpen={isOpen}
|
||||
className="room-settings"
|
||||
title={
|
||||
<Text variant="s1" weight="medium" primary>
|
||||
{isOpen && room.name}
|
||||
<span style={{ color: 'var(--tc-surface-low)' }}> — room settings</span>
|
||||
</Text>
|
||||
}
|
||||
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
|
||||
onRequestClose={requestClose}
|
||||
>
|
||||
{isOpen && (
|
||||
<div className="room-settings__content">
|
||||
<RoomProfile roomId={roomId} />
|
||||
<Tabs
|
||||
items={tabItems}
|
||||
defaultSelected={tabItems.findIndex((tab) => tab.text === selectedTab.text)}
|
||||
onSelect={handleTabChange}
|
||||
/>
|
||||
<div className="room-settings__cards-wrapper">
|
||||
{selectedTab.text === tabText.GENERAL && <GeneralSettings roomId={roomId} />}
|
||||
{selectedTab.text === tabText.MEMBERS && <RoomMembers roomId={roomId} />}
|
||||
{selectedTab.text === tabText.EMOJIS && <RoomEmojis roomId={roomId} />}
|
||||
{selectedTab.text === tabText.PERMISSIONS && <RoomPermissions roomId={roomId} />}
|
||||
{selectedTab.text === tabText.SECURITY && <SecuritySettings roomId={roomId} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PopupWindow>
|
||||
);
|
||||
}
|
||||
|
||||
export default RoomSettings;
|
||||
export { tabText };
|
|
@ -1,40 +0,0 @@
|
|||
@use '../../partials/dir';
|
||||
@use '../../partials/flex';
|
||||
|
||||
.room-settings {
|
||||
& .pw {
|
||||
background-color: var(--bg-surface-low);
|
||||
}
|
||||
|
||||
& .room-profile {
|
||||
padding: var(--sp-loose) var(--sp-extra-loose);
|
||||
}
|
||||
|
||||
& .tabs__content {
|
||||
padding: 0 var(--sp-normal);
|
||||
}
|
||||
|
||||
&__cards-wrapper {
|
||||
padding: 0 var(--sp-normal);
|
||||
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
|
||||
}
|
||||
}
|
||||
|
||||
.room-settings__card {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.room-settings .room-permissions__card,
|
||||
.room-settings .room-search__form,
|
||||
.room-settings .room-search__result-item,
|
||||
.room-settings .room-members {
|
||||
@extend .room-settings__card;
|
||||
}
|
|
@ -1,166 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './SpaceSettings.scss';
|
||||
|
||||
import cons from '../../../client/state/cons';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import Tabs from '../../atoms/tabs/Tabs';
|
||||
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||
import PopupWindow from '../../molecules/popup-window/PopupWindow';
|
||||
import RoomProfile from '../../molecules/room-profile/RoomProfile';
|
||||
import RoomVisibility from '../../molecules/room-visibility/RoomVisibility';
|
||||
import RoomAliases from '../../molecules/room-aliases/RoomAliases';
|
||||
import RoomPermissions from '../../molecules/room-permissions/RoomPermissions';
|
||||
import RoomMembers from '../../molecules/room-members/RoomMembers';
|
||||
import RoomEmojis from '../../molecules/room-emojis/RoomEmojis';
|
||||
|
||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||
import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
|
||||
import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg';
|
||||
import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
|
||||
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
|
||||
|
||||
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
|
||||
const tabText = {
|
||||
GENERAL: 'General',
|
||||
MEMBERS: 'Members',
|
||||
EMOJIS: 'Emojis',
|
||||
PERMISSIONS: 'Permissions',
|
||||
};
|
||||
|
||||
const tabItems = [
|
||||
{
|
||||
iconSrc: SettingsIC,
|
||||
text: tabText.GENERAL,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: UserIC,
|
||||
text: tabText.MEMBERS,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: EmojiIC,
|
||||
text: tabText.EMOJIS,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: ShieldUserIC,
|
||||
text: tabText.PERMISSIONS,
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
function GeneralSettings({ roomId }) {
|
||||
const mx = useMatrixClient();
|
||||
const roomName = mx.getRoom(roomId)?.name;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="room-settings__card">
|
||||
<MenuHeader>Options</MenuHeader>
|
||||
<MenuItem
|
||||
variant="danger"
|
||||
onClick={async () => {
|
||||
const isConfirmed = await confirmDialog(
|
||||
'Leave space',
|
||||
`Are you sure that you want to leave "${roomName}" space?`,
|
||||
'Leave',
|
||||
'danger'
|
||||
);
|
||||
if (isConfirmed) mx.leave(roomId);
|
||||
}}
|
||||
iconSrc={LeaveArrowIC}
|
||||
>
|
||||
Leave
|
||||
</MenuItem>
|
||||
</div>
|
||||
<div className="space-settings__card">
|
||||
<MenuHeader>Space visibility (who can join)</MenuHeader>
|
||||
<RoomVisibility roomId={roomId} />
|
||||
</div>
|
||||
<div className="space-settings__card">
|
||||
<MenuHeader>Space addresses</MenuHeader>
|
||||
<RoomAliases roomId={roomId} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
GeneralSettings.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function useWindowToggle(setSelectedTab) {
|
||||
const [window, setWindow] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const openSpaceSettings = (roomId, tab) => {
|
||||
setWindow({ roomId, tabText });
|
||||
const tabItem = tabItems.find((item) => item.text === tab);
|
||||
if (tabItem) setSelectedTab(tabItem);
|
||||
};
|
||||
navigation.on(cons.events.navigation.SPACE_SETTINGS_OPENED, openSpaceSettings);
|
||||
return () => {
|
||||
navigation.removeListener(cons.events.navigation.SPACE_SETTINGS_OPENED, openSpaceSettings);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const requestClose = () => setWindow(null);
|
||||
|
||||
return [window, requestClose];
|
||||
}
|
||||
|
||||
function SpaceSettings() {
|
||||
const [selectedTab, setSelectedTab] = useState(tabItems[0]);
|
||||
const [window, requestClose] = useWindowToggle(setSelectedTab);
|
||||
const isOpen = window !== null;
|
||||
const roomId = window?.roomId;
|
||||
|
||||
const mx = useMatrixClient();
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
const handleTabChange = (tabItem) => {
|
||||
setSelectedTab(tabItem);
|
||||
};
|
||||
|
||||
return (
|
||||
<PopupWindow
|
||||
isOpen={isOpen}
|
||||
className="space-settings"
|
||||
title={
|
||||
<Text variant="s1" weight="medium" primary>
|
||||
{isOpen && room.name}
|
||||
<span style={{ color: 'var(--tc-surface-low)' }}> — space settings</span>
|
||||
</Text>
|
||||
}
|
||||
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
|
||||
onRequestClose={requestClose}
|
||||
>
|
||||
{isOpen && (
|
||||
<div className="space-settings__content">
|
||||
<RoomProfile roomId={roomId} />
|
||||
<Tabs
|
||||
items={tabItems}
|
||||
defaultSelected={tabItems.findIndex((tab) => tab.text === selectedTab.text)}
|
||||
onSelect={handleTabChange}
|
||||
/>
|
||||
<div className="space-settings__cards-wrapper">
|
||||
{selectedTab.text === tabText.GENERAL && <GeneralSettings roomId={roomId} />}
|
||||
{selectedTab.text === tabText.MEMBERS && <RoomMembers roomId={roomId} />}
|
||||
{selectedTab.text === tabText.EMOJIS && <RoomEmojis roomId={roomId} />}
|
||||
{selectedTab.text === tabText.PERMISSIONS && <RoomPermissions roomId={roomId} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PopupWindow>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpaceSettings;
|
|
@ -1,37 +0,0 @@
|
|||
@use '../../partials/dir.scss';
|
||||
|
||||
.space-settings {
|
||||
& .pw {
|
||||
background-color: var(--bg-surface-low);
|
||||
}
|
||||
|
||||
& .room-profile {
|
||||
padding: var(--sp-loose) var(--sp-extra-loose);
|
||||
}
|
||||
|
||||
& .tabs__content {
|
||||
padding: 0 var(--sp-normal);
|
||||
}
|
||||
|
||||
&__cards-wrapper {
|
||||
padding: 0 var(--sp-normal);
|
||||
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
|
||||
}
|
||||
}
|
||||
|
||||
.space-settings__card {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.space-settings .room-permissions__card,
|
||||
.space-settings .room-members {
|
||||
@extend .space-settings__card;
|
||||
}
|
|
@ -1,13 +1,6 @@
|
|||
import appDispatcher from '../dispatcher';
|
||||
import cons from '../state/cons';
|
||||
|
||||
export function openSpaceSettings(roomId, tabText) {
|
||||
appDispatcher.dispatch({
|
||||
type: cons.actions.navigation.OPEN_SPACE_SETTINGS,
|
||||
roomId,
|
||||
tabText,
|
||||
});
|
||||
}
|
||||
|
||||
export function openSpaceAddExisting(roomId, spaces = false) {
|
||||
appDispatcher.dispatch({
|
||||
|
@ -17,14 +10,6 @@ export function openSpaceAddExisting(roomId, spaces = false) {
|
|||
});
|
||||
}
|
||||
|
||||
export function toggleRoomSettings(roomId, tabText) {
|
||||
appDispatcher.dispatch({
|
||||
type: cons.actions.navigation.TOGGLE_ROOM_SETTINGS,
|
||||
roomId,
|
||||
tabText
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function openCreateRoom(isSpace = false, parentId = null) {
|
||||
appDispatcher.dispatch({
|
||||
|
|
|
@ -6,22 +6,6 @@ const cons = {
|
|||
USER_ID: 'cinny_user_id',
|
||||
BASE_URL: 'cinny_hs_base_url',
|
||||
},
|
||||
DEVICE_DISPLAY_NAME: 'Cinny Web',
|
||||
IN_CINNY_SPACES: 'in.cinny.spaces',
|
||||
supportEventTypes: [
|
||||
'm.room.create',
|
||||
'm.room.message',
|
||||
'm.room.encrypted',
|
||||
'm.room.member',
|
||||
'm.sticker',
|
||||
],
|
||||
supportReceiptTypes: ['m.read', 'm.read.private'],
|
||||
notifs: {
|
||||
DEFAULT: 'default',
|
||||
ALL_MESSAGES: 'all_messages',
|
||||
MENTIONS_AND_KEYWORDS: 'mentions_and_keywords',
|
||||
MUTE: 'mute',
|
||||
},
|
||||
status: {
|
||||
PRE_FLIGHT: 'pre-flight',
|
||||
IN_FLIGHT: 'in-flight',
|
||||
|
@ -30,9 +14,7 @@ const cons = {
|
|||
},
|
||||
actions: {
|
||||
navigation: {
|
||||
OPEN_SPACE_SETTINGS: 'OPEN_SPACE_SETTINGS',
|
||||
OPEN_SPACE_ADDEXISTING: 'OPEN_SPACE_ADDEXISTING',
|
||||
TOGGLE_ROOM_SETTINGS: 'TOGGLE_ROOM_SETTINGS',
|
||||
OPEN_CREATE_ROOM: 'OPEN_CREATE_ROOM',
|
||||
OPEN_JOIN_ALIAS: 'OPEN_JOIN_ALIAS',
|
||||
OPEN_INVITE_USER: 'OPEN_INVITE_USER',
|
||||
|
@ -44,9 +26,7 @@ const cons = {
|
|||
},
|
||||
events: {
|
||||
navigation: {
|
||||
SPACE_SETTINGS_OPENED: 'SPACE_SETTINGS_OPENED',
|
||||
SPACE_ADDEXISTING_OPENED: 'SPACE_ADDEXISTING_OPENED',
|
||||
ROOM_SETTINGS_TOGGLED: 'ROOM_SETTINGS_TOGGLED',
|
||||
CREATE_ROOM_OPENED: 'CREATE_ROOM_OPENED',
|
||||
JOIN_ALIAS_OPENED: 'JOIN_ALIAS_OPENED',
|
||||
INVITE_USER_OPENED: 'INVITE_USER_OPENED',
|
||||
|
|
|
@ -19,19 +19,9 @@ class Navigation extends EventEmitter {
|
|||
|
||||
navigate(action) {
|
||||
const actions = {
|
||||
[cons.actions.navigation.OPEN_SPACE_SETTINGS]: () => {
|
||||
this.emit(cons.events.navigation.SPACE_SETTINGS_OPENED, action.roomId, action.tabText);
|
||||
},
|
||||
[cons.actions.navigation.OPEN_SPACE_ADDEXISTING]: () => {
|
||||
this.emit(cons.events.navigation.SPACE_ADDEXISTING_OPENED, action.roomId, action.spaces);
|
||||
},
|
||||
[cons.actions.navigation.TOGGLE_ROOM_SETTINGS]: () => {
|
||||
this.emit(
|
||||
cons.events.navigation.ROOM_SETTINGS_TOGGLED,
|
||||
action.roomId,
|
||||
action.tabText
|
||||
);
|
||||
},
|
||||
[cons.actions.navigation.OPEN_CREATE_ROOM]: () => {
|
||||
this.emit(
|
||||
cons.events.navigation.CREATE_ROOM_OPENED,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue