Compare commits

...

3 commits

Author SHA1 Message Date
array-in-a-matrix 60e9f22066 remove twemoji 2023-03-21 17:07:35 -04:00
array-in-a-matrix d21f008bf9 remove twemoji 2023-03-21 16:50:39 -04:00
array-in-a-matrix 7f7b248a0e remove twemoji 2023-03-21 16:23:14 -04:00
24 changed files with 867 additions and 770 deletions

View file

@ -2,17 +2,13 @@ import React from 'react';
import PropTypes from 'prop-types';
import './Avatar.scss';
import { twemojify } from '../../../util/twemojify';
import Text from '../text/Text';
import RawIcon from '../system-icons/RawIcon';
import ImageBrokenSVG from '../../../../public/res/svg/image-broken.svg';
import { avatarInitials } from '../../../util/common';
const Avatar = React.forwardRef(({
text, bgColor, iconSrc, iconColor, imageSrc, size,
}, ref) => {
const Avatar = React.forwardRef(({ text, bgColor, iconSrc, iconColor, imageSrc, size }, ref) => {
let textSize = 's1';
if (size === 'large') textSize = 'h1';
if (size === 'small') textSize = 'b1';
@ -20,34 +16,34 @@ const Avatar = React.forwardRef(({
return (
<div ref={ref} className={`avatar-container avatar-container__${size} noselect`}>
{
imageSrc !== null
? (
<img
draggable="false"
src={imageSrc}
onLoad={(e) => { e.target.style.backgroundColor = 'transparent'; }}
onError={(e) => { e.target.src = ImageBrokenSVG; }}
alt=""
/>
)
: (
<span
style={{ backgroundColor: iconSrc === null ? bgColor : 'transparent' }}
className={`avatar__border${iconSrc !== null ? '--active' : ''}`}
>
{
iconSrc !== null
? <RawIcon size={size} src={iconSrc} color={iconColor} />
: text !== null && (
<Text variant={textSize} primary>
{twemojify(avatarInitials(text))}
</Text>
)
}
</span>
)
}
{imageSrc !== null ? (
<img
draggable="false"
src={imageSrc}
onLoad={(e) => {
e.target.style.backgroundColor = 'transparent';
}}
onError={(e) => {
e.target.src = ImageBrokenSVG;
}}
alt=""
/>
) : (
<span
style={{ backgroundColor: iconSrc === null ? bgColor : 'transparent' }}
className={`avatar__border${iconSrc !== null ? '--active' : ''}`}
>
{iconSrc !== null ? (
<RawIcon size={size} src={iconSrc} color={iconColor} />
) : (
text !== null && (
<Text variant={textSize} primary>
{avatarInitials(text)}
</Text>
)
)}
</span>
)}
</div>
);
});

View file

@ -2,16 +2,21 @@ import React from 'react';
import PropTypes from 'prop-types';
import './Dialog.scss';
import { twemojify } from '../../../util/twemojify';
import Text from '../../atoms/text/Text';
import Header, { TitleWrapper } from '../../atoms/header/Header';
import ScrollView from '../../atoms/scroll/ScrollView';
import RawModal from '../../atoms/modal/RawModal';
function Dialog({
className, isOpen, title, onAfterOpen, onAfterClose,
contentOptions, onRequestClose, closeFromOutside, children,
className,
isOpen,
title,
onAfterOpen,
onAfterClose,
contentOptions,
onRequestClose,
closeFromOutside,
children,
invisibleScroll,
}) {
return (
@ -28,19 +33,19 @@ function Dialog({
<div className="dialog__content">
<Header>
<TitleWrapper>
{
typeof title === 'string'
? <Text variant="h2" weight="medium" primary>{twemojify(title)}</Text>
: title
}
{typeof title === 'string' ? (
<Text variant="h2" weight="medium" primary>
{title}
</Text>
) : (
title
)}
</TitleWrapper>
{contentOptions}
</Header>
<div className="dialog__content__wrapper">
<ScrollView autoHide={!invisibleScroll} invisible={invisibleScroll}>
<div className="dialog__content-container">
{children}
</div>
<div className="dialog__content-container">{children}</div>
</ScrollView>
</div>
</div>

View file

@ -1,21 +1,24 @@
/* eslint-disable react/prop-types */
import React, {
useState, useEffect, useCallback, useRef,
} from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import PropTypes from 'prop-types';
import './Message.scss';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import {
getUsername, getUsernameOfRoomMember, parseReply, trimHTMLReply,
getUsername,
getUsernameOfRoomMember,
parseReply,
trimHTMLReply,
} from '../../../util/matrixUtil';
import colorMXID from '../../../util/colorMXID';
import { getEventCords } from '../../../util/common';
import { redactEvent, sendReaction } from '../../../client/action/roomTimeline';
import {
openEmojiBoard, openProfileViewer, openReadReceipts, openViewSource, replyTo,
openEmojiBoard,
openProfileViewer,
openReadReceipts,
openViewSource,
replyTo,
} from '../../../client/action/navigation';
import { sanitizeCustomHtml } from '../../../util/sanitize';
@ -27,7 +30,11 @@ import Input from '../../atoms/input/Input';
import Avatar from '../../atoms/avatar/Avatar';
import IconButton from '../../atoms/button/IconButton';
import Time from '../../atoms/time/Time';
import ContextMenu, { MenuHeader, MenuItem, MenuBorder } from '../../atoms/context-menu/ContextMenu';
import ContextMenu, {
MenuHeader,
MenuItem,
MenuBorder,
} from '../../atoms/context-menu/ContextMenu';
import * as Media from '../media/Media';
import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg';
@ -61,9 +68,7 @@ function PlaceholderMessage() {
);
}
const MessageAvatar = React.memo(({
roomId, avatarSrc, userId, username,
}) => (
const MessageAvatar = React.memo(({ roomId, avatarSrc, userId, username }) => (
<div className="message__avatar-container">
<button type="button" onClick={() => openProfileViewer(userId, roomId)}>
<Avatar imageSrc={avatarSrc} text={username} bgColor={colorMXID(userId)} size="small" />
@ -71,9 +76,7 @@ const MessageAvatar = React.memo(({
</div>
));
const MessageHeader = React.memo(({
userId, username, timestamp, fullTime,
}) => (
const MessageHeader = React.memo(({ userId, username, timestamp, fullTime }) => (
<div className="message__header">
<Text
style={{ color: colorMXID(userId) }}
@ -82,8 +85,8 @@ const MessageHeader = React.memo(({
weight="medium"
span
>
<span>{twemojify(username)}</span>
<span>{twemojify(userId)}</span>
<span>{username}</span>
<span>{userId}</span>
</Text>
<div className="message__time">
<Text variant="b3">
@ -107,9 +110,7 @@ function MessageReply({ name, color, body }) {
<div className="message__reply">
<Text variant="b2">
<RawIcon color={color} size="extra-small" src={ReplyArrowIC} />
<span style={{ color }}>{twemojify(name)}</span>
{' '}
{twemojify(body)}
<span style={{ color }}>{name}</span> {body}
</Text>
</div>
);
@ -143,7 +144,9 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
const username = getUsernameOfRoomMember(mEvent.sender);
if (isMountedRef.current === false) return;
const fallbackBody = mEvent.isRedacted() ? '*** This message has been deleted ***' : '*** Unable to load reply ***';
const fallbackBody = mEvent.isRedacted()
? '*** This message has been deleted ***'
: '*** Unable to load reply ***';
let parsedBody = parseReply(rawBody)?.body ?? rawBody ?? fallbackBody;
if (editedList && parsedBody.startsWith(' * ')) {
parsedBody = parsedBody.slice(3);
@ -197,32 +200,20 @@ MessageReplyWrapper.propTypes = {
eventId: PropTypes.string.isRequired,
};
const MessageBody = React.memo(({
senderName,
body,
isCustomHTML,
isEdited,
msgType,
}) => {
const MessageBody = React.memo(({ senderName, body, isCustomHTML, isEdited, msgType }) => {
// if body is not string it is a React element.
if (typeof body !== 'string') return <div className="message__body">{body}</div>;
let content = null;
if (isCustomHTML) {
try {
content = twemojify(
sanitizeCustomHtml(initMatrix.matrixClient, body),
undefined,
true,
false,
true,
);
content = sanitizeCustomHtml(initMatrix.matrixClient, body);
} catch {
console.error('Malformed custom html: ', body);
content = twemojify(body, undefined);
content = body;
}
} else {
content = twemojify(body, undefined, true);
content = body;
}
// Determine if this message should render with large emojis
@ -240,10 +231,14 @@ const MessageBody = React.memo(({
const nEmojis = content.filter((e) => e.type === 'img').length;
// Make sure there's no text besides whitespace and variation selector U+FE0F
if (nEmojis <= 10 && content.every((element) => (
(typeof element === 'object' && element.type === 'img')
|| (typeof element === 'string' && /^[\s\ufe0f]*$/g.test(element))
))) {
if (
nEmojis <= 10 &&
content.every(
(element) =>
(typeof element === 'object' && element.type === 'img') ||
(typeof element === 'string' && /^[\s\ufe0f]*$/g.test(element))
)
) {
emojiOnly = true;
}
}
@ -251,22 +246,25 @@ const MessageBody = React.memo(({
if (!isCustomHTML) {
// If this is a plaintext message, wrap it in a <p> element (automatically applying
// white-space: pre-wrap) in order to preserve newlines
content = (<p className="message__body-plain">{content}</p>);
content = <p className="message__body-plain">{content}</p>;
}
return (
<div className="message__body">
<div dir="auto" className={`text ${emojiOnly ? 'text-h1' : 'text-b1'}`}>
{ msgType === 'm.emote' && (
{msgType === 'm.emote' && (
<>
{'* '}
{twemojify(senderName)}
{' '}
{senderName}{' '}
</>
)}
{ content }
{content}
</div>
{ isEdited && <Text className="message__body-edited" variant="b3">(edited)</Text>}
{isEdited && (
<Text className="message__body-edited" variant="b3">
(edited)
</Text>
)}
</div>
);
});
@ -305,7 +303,13 @@ function MessageEdit({ body, onSave, onCancel }) {
};
return (
<form className="message__edit" onSubmit={(e) => { e.preventDefault(); onSave(editInputRef.current.value, body); }}>
<form
className="message__edit"
onSubmit={(e) => {
e.preventDefault();
onSave(editInputRef.current.value, body);
}}
>
<Input
forwardRef={editInputRef}
onKeyDown={handleKeyDown}
@ -316,7 +320,9 @@ function MessageEdit({ body, onSave, onCancel }) {
autoFocus
/>
<div className="message__edit-btns">
<Button type="submit" variant="primary">Save</Button>
<Button type="submit" variant="primary">
Save
</Button>
<Button onClick={onCancel}>Cancel</Button>
</div>
</form>
@ -366,23 +372,19 @@ function genReactionMsg(userIds, reaction, shortcode) {
<>
{userIds.map((userId, index) => (
<React.Fragment key={userId}>
{twemojify(getUsername(userId))}
{getUsername(userId)}
{index < userIds.length - 1 && (
<span style={{ opacity: '.6' }}>
{index === userIds.length - 2 ? ' and ' : ', '}
</span>
<span style={{ opacity: '.6' }}>{index === userIds.length - 2 ? ' and ' : ', '}</span>
)}
</React.Fragment>
))}
<span style={{ opacity: '.6' }}>{' reacted with '}</span>
{twemojify(shortcode ? `:${shortcode}:` : reaction, { className: 'react-emoji' })}
{(shortcode ? `:${shortcode}:` : reaction, { className: 'react-emoji' })}
</>
);
}
function MessageReaction({
reaction, shortcode, count, users, isActive, onClick,
}) {
function MessageReaction({ reaction, shortcode, count, users, isActive, onClick }) {
let customEmojiUrl = null;
if (reaction.match(/^mxc:\/\/\S+$/)) {
customEmojiUrl = initMatrix.matrixClient.mxcUrlToHttp(reaction);
@ -390,19 +392,32 @@ function MessageReaction({
return (
<Tooltip
className="msg__reaction-tooltip"
content={<Text variant="b2">{users.length > 0 ? genReactionMsg(users, reaction, shortcode) : 'Unable to load who has reacted'}</Text>}
content={
<Text variant="b2">
{users.length > 0
? genReactionMsg(users, reaction, shortcode)
: 'Unable to load who has reacted'}
</Text>
}
>
<button
onClick={onClick}
type="button"
className={`msg__reaction${isActive ? ' msg__reaction--active' : ''}`}
>
{
customEmojiUrl
? <img className="react-emoji" draggable="false" alt={shortcode ?? reaction} src={customEmojiUrl} />
: twemojify(reaction, { className: 'react-emoji' })
}
<Text variant="b3" className="msg__reaction-count">{count}</Text>
{customEmojiUrl ? (
<img
className="react-emoji"
draggable="false"
alt={shortcode ?? reaction}
src={customEmojiUrl}
/>
) : (
(reaction, { className: 'react-emoji' })
)}
<Text variant="b3" className="msg__reaction-count">
{count}
</Text>
</button>
</Tooltip>
);
@ -468,21 +483,19 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
return (
<div className="message__reactions text text-b3 noselect">
{
Object.keys(reactions).map((key) => (
<MessageReaction
key={key}
reaction={key}
shortcode={reactions[key].shortcode}
count={reactions[key].count}
users={reactions[key].users}
isActive={reactions[key].isActive}
onClick={() => {
toggleEmoji(roomId, mEvent.getId(), key, reactions[key].shortcode, roomTimeline);
}}
/>
))
}
{Object.keys(reactions).map((key) => (
<MessageReaction
key={key}
reaction={key}
shortcode={reactions[key].shortcode}
count={reactions[key].count}
users={reactions[key].users}
isActive={reactions[key].isActive}
onClick={() => {
toggleEmoji(roomId, mEvent.getId(), key, reactions[key].shortcode, roomTimeline);
}}
/>
))}
{canSendReaction && (
<IconButton
onClick={(e) => {
@ -503,11 +516,11 @@ MessageReactionGroup.propTypes = {
function isMedia(mE) {
return (
mE.getContent()?.msgtype === 'm.file'
|| mE.getContent()?.msgtype === 'm.image'
|| mE.getContent()?.msgtype === 'm.audio'
|| mE.getContent()?.msgtype === 'm.video'
|| mE.getType() === 'm.sticker'
mE.getContent()?.msgtype === 'm.file' ||
mE.getContent()?.msgtype === 'm.image' ||
mE.getContent()?.msgtype === 'm.audio' ||
mE.getContent()?.msgtype === 'm.video' ||
mE.getType() === 'm.sticker'
);
}
@ -523,9 +536,7 @@ function handleOpenViewSource(mEvent, roomTimeline) {
openViewSource(editedMEvent !== undefined ? editedMEvent : mEvent);
}
const MessageOptions = React.memo(({
roomTimeline, mEvent, edit, reply,
}) => {
const MessageOptions = React.memo(({ roomTimeline, mEvent, edit, reply }) => {
const { roomId, room } = roomTimeline;
const mx = initMatrix.matrixClient;
const senderId = mEvent.getSender();
@ -544,19 +555,9 @@ const MessageOptions = React.memo(({
tooltip="Add reaction"
/>
)}
<IconButton
onClick={() => reply()}
src={ReplyArrowIC}
size="extra-small"
tooltip="Reply"
/>
{(senderId === mx.getUserId() && !isMedia(mEvent)) && (
<IconButton
onClick={() => edit(true)}
src={PencilIC}
size="extra-small"
tooltip="Edit"
/>
<IconButton onClick={() => reply()} src={ReplyArrowIC} size="extra-small" tooltip="Reply" />
{senderId === mx.getUserId() && !isMedia(mEvent) && (
<IconButton onClick={() => edit(true)} src={PencilIC} size="extra-small" tooltip="Edit" />
)}
<ContextMenu
content={() => (
@ -568,10 +569,7 @@ const MessageOptions = React.memo(({
>
Read receipts
</MenuItem>
<MenuItem
iconSrc={CmdIC}
onClick={() => handleOpenViewSource(mEvent, roomTimeline)}
>
<MenuItem iconSrc={CmdIC} onClick={() => handleOpenViewSource(mEvent, roomTimeline)}>
View source
</MenuItem>
{(canIRedact || senderId === mx.getUserId()) && (
@ -585,7 +583,7 @@ const MessageOptions = React.memo(({
'Delete message',
'Are you sure that you want to delete this message?',
'Delete',
'danger',
'danger'
);
if (!isConfirmed) return;
redactEvent(roomId, mEvent.getId());
@ -619,7 +617,8 @@ MessageOptions.propTypes = {
function genMediaContent(mE) {
const mx = initMatrix.matrixClient;
const mContent = mE.getContent();
if (!mContent || !mContent.body) return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
if (!mContent || !mContent.body)
return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
let mediaMXC = mContent?.url;
const isEncryptedFile = typeof mediaMXC === 'undefined';
@ -627,7 +626,8 @@ function genMediaContent(mE) {
let thumbnailMXC = mContent?.info?.thumbnail_url;
if (typeof mediaMXC === 'undefined' || mediaMXC === '') return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
if (typeof mediaMXC === 'undefined' || mediaMXC === '')
return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
let msgType = mE.getContent()?.msgtype;
const safeMimetype = getBlobSafeMimeType(mContent.info?.mimetype);
@ -717,13 +717,19 @@ function getEditedBody(editedMEvent) {
}
function Message({
mEvent, isBodyOnly, roomTimeline,
focus, fullTime, isEdit, setEdit, cancelEdit,
mEvent,
isBodyOnly,
roomTimeline,
focus,
fullTime,
isEdit,
setEdit,
cancelEdit,
}) {
const roomId = mEvent.getRoomId();
const { editedTimeline, reactionTimeline } = roomTimeline ?? {};
const className = ['message', (isBodyOnly ? 'message--body-only' : 'message--full')];
const className = ['message', isBodyOnly ? 'message--body-only' : 'message--full'];
if (focus) className.push('message--focus');
const content = mEvent.getContent();
const eventId = mEvent.getId();
@ -731,7 +737,8 @@ function Message({
const senderId = mEvent.getSender();
let { body } = content;
const username = mEvent.sender ? getUsernameOfRoomMember(mEvent.sender) : getUsername(senderId);
const avatarSrc = mEvent.sender?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop') ?? null;
const avatarSrc =
mEvent.sender?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop') ?? null;
let isCustomHTML = content.format === 'org.matrix.custom.html';
let customHTML = isCustomHTML ? content.formatted_body : null;
@ -765,18 +772,16 @@ function Message({
return (
<div className={className.join(' ')}>
{
isBodyOnly
? <div className="message__avatar-container" />
: (
<MessageAvatar
roomId={roomId}
avatarSrc={avatarSrc}
userId={senderId}
username={username}
/>
)
}
{isBodyOnly ? (
<div className="message__avatar-container" />
) : (
<MessageAvatar
roomId={roomId}
avatarSrc={avatarSrc}
userId={senderId}
username={username}
/>
)}
<div className="message__main-container">
{!isBodyOnly && (
<MessageHeader
@ -787,10 +792,7 @@ function Message({
/>
)}
{roomTimeline && isReply && (
<MessageReplyWrapper
roomTimeline={roomTimeline}
eventId={mEvent.replyEventId}
/>
<MessageReplyWrapper roomTimeline={roomTimeline} eventId={mEvent.replyEventId} />
)}
{!isEdit && (
<MessageBody
@ -803,9 +805,11 @@ function Message({
)}
{isEdit && (
<MessageEdit
body={(customHTML
? html(customHTML, { kind: 'edit', onlyPlain: true }).plain
: plain(body, { kind: 'edit', onlyPlain: true }).plain)}
body={
customHTML
? html(customHTML, { kind: 'edit', onlyPlain: true }).plain
: plain(body, { kind: 'edit', onlyPlain: true }).plain
}
onSave={(newBody, oldBody) => {
if (newBody !== oldBody) {
initMatrix.roomsInput.sendEditedMessage(roomId, mEvent, newBody);
@ -815,16 +819,9 @@ function Message({
onCancel={cancelEdit}
/>
)}
{haveReactions && (
<MessageReactionGroup roomTimeline={roomTimeline} mEvent={mEvent} />
)}
{haveReactions && <MessageReactionGroup roomTimeline={roomTimeline} mEvent={mEvent} />}
{roomTimeline && !isEdit && (
<MessageOptions
roomTimeline={roomTimeline}
mEvent={mEvent}
edit={edit}
reply={reply}
/>
<MessageOptions roomTimeline={roomTimeline} mEvent={mEvent} edit={edit} reply={reply} />
)}
</div>
</div>

View file

@ -2,16 +2,12 @@ import React from 'react';
import PropTypes from 'prop-types';
import './PeopleSelector.scss';
import { twemojify } from '../../../util/twemojify';
import { blurOnBubbling } from '../../atoms/button/script';
import Text from '../../atoms/text/Text';
import Avatar from '../../atoms/avatar/Avatar';
function PeopleSelector({
avatarSrc, name, color, peopleRole, onClick,
}) {
function PeopleSelector({ avatarSrc, name, color, peopleRole, onClick }) {
return (
<div className="people-selector__container">
<button
@ -21,8 +17,14 @@ function PeopleSelector({
type="button"
>
<Avatar imageSrc={avatarSrc} text={name} bgColor={color} size="extra-small" />
<Text className="people-selector__name" variant="b1">{twemojify(name)}</Text>
{peopleRole !== null && <Text className="people-selector__role" variant="b3">{peopleRole}</Text>}
<Text className="people-selector__name" variant="b1">
{name}
</Text>
{peopleRole !== null && (
<Text className="people-selector__role" variant="b3">
{peopleRole}
</Text>
)}
</button>
</div>
);

View file

@ -2,8 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import './PopupWindow.scss';
import { twemojify } from '../../../util/twemojify';
import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import { MenuItem } from '../../atoms/context-menu/ContextMenu';
@ -13,19 +11,11 @@ import RawModal from '../../atoms/modal/RawModal';
import ChevronLeftIC from '../../../../public/res/ic/outlined/chevron-left.svg';
function PWContentSelector({
selected, variant, iconSrc,
type, onClick, children,
}) {
function PWContentSelector({ selected, variant, iconSrc, type, onClick, children }) {
const pwcsClass = selected ? ' pw-content-selector--selected' : '';
return (
<div className={`pw-content-selector${pwcsClass}`}>
<MenuItem
variant={variant}
iconSrc={iconSrc}
type={type}
onClick={onClick}
>
<MenuItem variant={variant} iconSrc={iconSrc} type={type} onClick={onClick}>
{children}
</MenuItem>
</div>
@ -49,9 +39,16 @@ PWContentSelector.propTypes = {
};
function PopupWindow({
className, isOpen, title, contentTitle,
drawer, drawerOptions, contentOptions,
onAfterClose, onRequestClose, children,
className,
isOpen,
title,
contentTitle,
drawer,
drawerOptions,
contentOptions,
onAfterClose,
onRequestClose,
children,
}) {
const haveDrawer = drawer !== null;
const cTitle = contentTitle !== null ? contentTitle : title;
@ -69,21 +66,26 @@ function PopupWindow({
{haveDrawer && (
<div className="pw__drawer">
<Header>
<IconButton size="small" src={ChevronLeftIC} onClick={onRequestClose} tooltip="Back" />
<IconButton
size="small"
src={ChevronLeftIC}
onClick={onRequestClose}
tooltip="Back"
/>
<TitleWrapper>
{
typeof title === 'string'
? <Text variant="s1" weight="medium" primary>{twemojify(title)}</Text>
: title
}
{typeof title === 'string' ? (
<Text variant="s1" weight="medium" primary>
{title}
</Text>
) : (
title
)}
</TitleWrapper>
{drawerOptions}
</Header>
<div className="pw__drawer__content__wrapper">
<ScrollView invisible>
<div className="pw__drawer__content">
{drawer}
</div>
<div className="pw__drawer__content">{drawer}</div>
</ScrollView>
</div>
</div>
@ -91,19 +93,19 @@ function PopupWindow({
<div className="pw__content">
<Header>
<TitleWrapper>
{
typeof cTitle === 'string'
? <Text variant="h2" weight="medium" primary>{twemojify(cTitle)}</Text>
: cTitle
}
{typeof cTitle === 'string' ? (
<Text variant="h2" weight="medium" primary>
{cTitle}
</Text>
) : (
cTitle
)}
</TitleWrapper>
{contentOptions}
</Header>
<div className="pw__content__wrapper">
<ScrollView autoHide>
<div className="pw__content-container">
{children}
</div>
<div className="pw__content-container">{children}</div>
</ScrollView>
</div>
</div>

View file

@ -1,8 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import { openInviteUser } from '../../../client/action/navigation';
import * as roomActions from '../../../client/action/room';
@ -37,7 +35,7 @@ function RoomOptions({ roomId, afterOptionSelect }) {
'Leave room',
`Are you sure that you want to leave "${room.name}" room?`,
'Leave',
'danger',
'danger'
);
if (!isConfirmed) return;
roomActions.leave(roomId);
@ -45,16 +43,16 @@ function RoomOptions({ roomId, afterOptionSelect }) {
return (
<div style={{ maxWidth: '256px' }}>
<MenuHeader>{twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)}</MenuHeader>
<MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>Mark as read</MenuItem>
<MenuItem
iconSrc={AddUserIC}
onClick={handleInviteClick}
disabled={!canInvite}
>
<MenuHeader>{`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`}</MenuHeader>
<MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>
Mark as read
</MenuItem>
<MenuItem iconSrc={AddUserIC} onClick={handleInviteClick} disabled={!canInvite}>
Invite
</MenuItem>
<MenuItem iconSrc={LeaveArrowIC} variant="danger" onClick={handleLeaveClick}>Leave</MenuItem>
<MenuItem iconSrc={LeaveArrowIC} variant="danger" onClick={handleLeaveClick}>
Leave
</MenuItem>
<MenuHeader>Notification</MenuHeader>
<RoomNotification roomId={roomId} />
</div>

View file

@ -2,8 +2,6 @@ import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './RoomProfile.scss';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import colorMXID from '../../../util/colorMXID';
@ -33,7 +31,9 @@ function RoomProfile({ roomId }) {
const mx = initMatrix.matrixClient;
const isDM = initMatrix.roomList.directs.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;
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;
@ -122,7 +122,7 @@ function RoomProfile({ roomId }) {
'Remove avatar',
'Are you sure that you want to remove room avatar?',
'Remove',
'caution',
'caution'
);
if (isConfirmed) {
await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
@ -132,15 +132,45 @@ function RoomProfile({ roomId }) {
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 && (
{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 type="submit" variant="primary">
Save
</Button>
<Button onClick={handleCancelEditing}>Cancel</Button>
</div>
)}
@ -148,10 +178,15 @@ function RoomProfile({ roomId }) {
);
const renderNameAndTopic = () => (
<div className="room-profile__display" style={{ marginBottom: avatarSrc && canChangeAvatar ? '24px' : '0' }}>
<div
className="room-profile__display"
style={{ marginBottom: avatarSrc && canChangeAvatar ? '24px' : '0' }}
>
<div>
<Text variant="h2" weight="medium" primary>{twemojify(roomName)}</Text>
{ (canChangeName || canChangeTopic) && (
<Text variant="h2" weight="medium" primary>
{roomName}
</Text>
{(canChangeName || canChangeTopic) && (
<IconButton
src={PencilIC}
size="extra-small"
@ -161,15 +196,17 @@ function RoomProfile({ roomId }) {
)}
</div>
<Text variant="b3">{room.getCanonicalAlias() || room.roomId}</Text>
{roomTopic && <Text variant="b2">{twemojify(roomTopic, undefined, true)}</Text>}
{roomTopic && <Text variant="b2">{roomTopic}</Text>}
</div>
);
return (
<div className="room-profile">
<div className="room-profile__content">
{ !canChangeAvatar && <Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="large" />}
{ canChangeAvatar && (
{!canChangeAvatar && (
<Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="large" />
)}
{canChangeAvatar && (
<ImageUpload
text={roomName}
bgColor={colorMXID(roomId)}

View file

@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import './RoomSelector.scss';
import { twemojify } from '../../../util/twemojify';
import colorMXID from '../../../util/colorMXID';
import Text from '../../atoms/text/Text';
@ -11,8 +10,13 @@ import NotificationBadge from '../../atoms/badge/NotificationBadge';
import { blurOnBubbling } from '../../atoms/button/script';
function RoomSelectorWrapper({
isSelected, isMuted, isUnread, onClick,
content, options, onContextMenu,
isSelected,
isMuted,
isUnread,
onClick,
content,
options,
onContextMenu,
}) {
const classes = ['room-selector'];
if (isMuted) classes.push('room-selector--muted');
@ -50,16 +54,26 @@ RoomSelectorWrapper.propTypes = {
};
function RoomSelector({
name, parentName, roomId, imageSrc, iconSrc,
isSelected, isMuted, isUnread, notificationCount, isAlert,
options, onClick, onContextMenu,
name,
parentName,
roomId,
imageSrc,
iconSrc,
isSelected,
isMuted,
isUnread,
notificationCount,
isAlert,
options,
onClick,
onContextMenu,
}) {
return (
<RoomSelectorWrapper
isSelected={isSelected}
isMuted={isMuted}
isUnread={isUnread}
content={(
content={
<>
<Avatar
text={name}
@ -70,22 +84,22 @@ function RoomSelector({
size="extra-small"
/>
<Text variant="b1" weight={isUnread ? 'medium' : 'normal'}>
{twemojify(name)}
{name}
{parentName && (
<Text variant="b3" span>
{' — '}
{twemojify(parentName)}
{parentName}
</Text>
)}
</Text>
{ isUnread && (
{isUnread && (
<NotificationBadge
alert={isAlert}
content={notificationCount !== 0 ? notificationCount : null}
/>
)}
</>
)}
}
options={options}
onClick={onClick}
onContextMenu={onContextMenu}
@ -110,10 +124,7 @@ RoomSelector.propTypes = {
isSelected: PropTypes.bool,
isMuted: PropTypes.bool,
isUnread: PropTypes.bool.isRequired,
notificationCount: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
notificationCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
isAlert: PropTypes.bool.isRequired,
options: PropTypes.node,
onClick: PropTypes.func.isRequired,

View file

@ -2,38 +2,32 @@ import React from 'react';
import PropTypes from 'prop-types';
import './SidebarAvatar.scss';
import { twemojify } from '../../../util/twemojify';
import Text from '../../atoms/text/Text';
import Tooltip from '../../atoms/tooltip/Tooltip';
import { blurOnBubbling } from '../../atoms/button/script';
const SidebarAvatar = React.forwardRef(({
className, tooltip, active, onClick,
onContextMenu, avatar, notificationBadge,
}, ref) => {
const classes = ['sidebar-avatar'];
if (active) classes.push('sidebar-avatar--active');
if (className) classes.push(className);
return (
<Tooltip
content={<Text variant="b1">{twemojify(tooltip)}</Text>}
placement="right"
>
<button
ref={ref}
className={classes.join(' ')}
type="button"
onMouseUp={(e) => blurOnBubbling(e, '.sidebar-avatar')}
onClick={onClick}
onContextMenu={onContextMenu}
>
{avatar}
{notificationBadge}
</button>
</Tooltip>
);
});
const SidebarAvatar = React.forwardRef(
({ className, tooltip, active, onClick, onContextMenu, avatar, notificationBadge }, ref) => {
const classes = ['sidebar-avatar'];
if (active) classes.push('sidebar-avatar--active');
if (className) classes.push(className);
return (
<Tooltip content={<Text variant="b1">{tooltip}</Text>} placement="right">
<button
ref={ref}
className={classes.join(' ')}
type="button"
onMouseUp={(e) => blurOnBubbling(e, '.sidebar-avatar')}
onClick={onClick}
onContextMenu={onContextMenu}
>
{avatar}
{notificationBadge}
</button>
</Tooltip>
);
}
);
SidebarAvatar.defaultProps = {
className: null,
active: false,

View file

@ -2,8 +2,6 @@ import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './SpaceAddExisting.scss';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
@ -33,14 +31,12 @@ function SpaceAddExistingContent({ roomId }) {
const [selected, setSelected] = useState([]);
const [searchIds, setSearchIds] = useState(null);
const mx = initMatrix.matrixClient;
const {
spaces, rooms, directs, roomIdToParents,
} = initMatrix.roomList;
const { spaces, rooms, directs, roomIdToParents } = initMatrix.roomList;
useEffect(() => {
const allIds = [...spaces, ...rooms, ...directs].filter((rId) => (
rId !== roomId && !roomIdToParents.get(rId)?.has(roomId)
));
const allIds = [...spaces, ...rooms, ...directs].filter(
(rId) => rId !== roomId && !roomIdToParents.get(rId)?.has(roomId)
);
setAllRoomIds(allIds);
}, [roomId]);
@ -68,20 +64,25 @@ function SpaceAddExistingContent({ roomId }) {
via.push(getIdServer(rId));
}
return mx.sendStateEvent(roomId, 'm.space.child', {
auto_join: false,
suggested: false,
via,
}, rId);
return mx.sendStateEvent(
roomId,
'm.space.child',
{
auto_join: false,
suggested: false,
via,
},
rId
);
});
mountStore.setItem(true);
await Promise.allSettled(promises);
if (mountStore.getItem() !== true) return;
const allIds = [...spaces, ...rooms, ...directs].filter((rId) => (
rId !== roomId && !roomIdToParents.get(rId)?.has(roomId) && !selected.includes(rId)
));
const allIds = [...spaces, ...rooms, ...directs].filter(
(rId) => rId !== roomId && !roomIdToParents.get(rId)?.has(roomId) && !selected.includes(rId)
);
setAllRoomIds(allIds);
setProcess(null);
setSelected([]);
@ -98,9 +99,7 @@ function SpaceAddExistingContent({ roomId }) {
const searchedIds = allRoomIds.filter((rId) => {
let name = mx.getRoom(rId)?.name;
if (!name) return false;
name = name.normalize('NFKC')
.toLocaleLowerCase()
.replace(/\s/g, '');
name = name.normalize('NFKC').toLocaleLowerCase().replace(/\s/g, '');
return name.includes(term);
});
setSearchIds(searchedIds);
@ -114,66 +113,64 @@ function SpaceAddExistingContent({ roomId }) {
return (
<>
<form onSubmit={(ev) => { ev.preventDefault(); }}>
<form
onSubmit={(ev) => {
ev.preventDefault();
}}
>
<RawIcon size="small" src={SearchIC} />
<Input
name="searchInput"
onChange={handleSearch}
placeholder="Search room"
autoFocus
/>
<Input name="searchInput" onChange={handleSearch} placeholder="Search room" autoFocus />
<IconButton size="small" type="button" onClick={handleSearchClear} src={CrossIC} />
</form>
{searchIds?.length === 0 && <Text>No results found</Text>}
{
(searchIds || allRoomIds).map((rId) => {
const room = mx.getRoom(rId);
let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
{(searchIds || allRoomIds).map((rId) => {
const room = mx.getRoom(rId);
let imageSrc =
room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
const parentSet = roomIdToParents.get(rId);
const parentNames = parentSet
? [...parentSet].map((parentId) => mx.getRoom(parentId).name)
: undefined;
const parents = parentNames ? parentNames.join(', ') : null;
const parentSet = roomIdToParents.get(rId);
const parentNames = parentSet
? [...parentSet].map((parentId) => mx.getRoom(parentId).name)
: undefined;
const parents = parentNames ? parentNames.join(', ') : null;
const handleSelect = () => toggleSelection(rId);
const handleSelect = () => toggleSelection(rId);
return (
<RoomSelector
key={rId}
name={room.name}
parentName={parents}
roomId={rId}
imageSrc={directs.has(rId) ? imageSrc : null}
iconSrc={
directs.has(rId)
? null
: joinRuleToIconSrc(room.getJoinRule(), room.isSpaceRoom())
}
isUnread={false}
notificationCount={0}
isAlert={false}
onClick={handleSelect}
options={(
<Checkbox
isActive={selected.includes(rId)}
variant="positive"
onToggle={handleSelect}
tabIndex={-1}
disabled={process !== null}
/>
)}
/>
);
})
}
return (
<RoomSelector
key={rId}
name={room.name}
parentName={parents}
roomId={rId}
imageSrc={directs.has(rId) ? imageSrc : null}
iconSrc={
directs.has(rId) ? null : joinRuleToIconSrc(room.getJoinRule(), room.isSpaceRoom())
}
isUnread={false}
notificationCount={0}
isAlert={false}
onClick={handleSelect}
options={
<Checkbox
isActive={selected.includes(rId)}
variant="positive"
onToggle={handleSelect}
tabIndex={-1}
disabled={process !== null}
/>
}
/>
);
})}
{selected.length !== 0 && (
<div className="space-add-existing__footer">
{process && <Spinner size="small" />}
<Text weight="medium">{process || `${selected.length} item selected`}</Text>
{ !process && (
<Button onClick={handleAdd} variant="primary">Add</Button>
{!process && (
<Button onClick={handleAdd} variant="primary">
Add
</Button>
)}
</div>
)}
@ -209,20 +206,16 @@ function SpaceAddExisting() {
<Dialog
isOpen={roomId !== null}
className="space-add-existing"
title={(
title={
<Text variant="s1" weight="medium" primary>
{roomId && twemojify(room.name)}
{roomId && room.name}
<span style={{ color: 'var(--tc-surface-low)' }}> add existing rooms</span>
</Text>
)}
}
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
onRequestClose={requestClose}
>
{
roomId
? <SpaceAddExistingContent roomId={roomId} />
: <div />
}
{roomId ? <SpaceAddExistingContent roomId={roomId} /> : <div />}
</Dialog>
);
}

View file

@ -1,10 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import { openSpaceSettings, openSpaceManage, openInviteUser } from '../../../client/action/navigation';
import {
openSpaceSettings,
openSpaceManage,
openInviteUser,
} from '../../../client/action/navigation';
import { markAsRead } from '../../../client/action/notifications';
import { leave } from '../../../client/action/room';
import {
@ -74,7 +76,7 @@ function SpaceOptions({ roomId, afterOptionSelect }) {
'Leave space',
`Are you sure that you want to leave "${room.name}" space?`,
'Leave',
'danger',
'danger'
);
if (!isConfirmed) return;
leave(roomId);
@ -82,34 +84,29 @@ function SpaceOptions({ roomId, afterOptionSelect }) {
return (
<div style={{ maxWidth: 'calc(var(--navigation-drawer-width) - var(--sp-normal))' }}>
<MenuHeader>{twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)}</MenuHeader>
<MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>Mark as read</MenuItem>
<MenuHeader>{`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`}</MenuHeader>
<MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>
Mark as read
</MenuItem>
<MenuItem
onClick={handleCategorizeClick}
iconSrc={isCategorized ? CategoryFilledIC : CategoryIC}
>
{isCategorized ? 'Uncategorize subspaces' : 'Categorize subspaces'}
</MenuItem>
<MenuItem
onClick={handlePinClick}
iconSrc={isPinned ? PinFilledIC : PinIC}
>
<MenuItem onClick={handlePinClick} iconSrc={isPinned ? PinFilledIC : PinIC}>
{isPinned ? 'Unpin from sidebar' : 'Pin to sidebar'}
</MenuItem>
<MenuItem
iconSrc={AddUserIC}
onClick={handleInviteClick}
disabled={!canInvite}
>
<MenuItem iconSrc={AddUserIC} onClick={handleInviteClick} disabled={!canInvite}>
Invite
</MenuItem>
<MenuItem onClick={handleManageRoom} iconSrc={HashSearchIC}>Manage rooms</MenuItem>
<MenuItem onClick={handleSettingsClick} iconSrc={SettingsIC}>Settings</MenuItem>
<MenuItem
variant="danger"
onClick={handleLeaveClick}
iconSrc={LeaveArrowIC}
>
<MenuItem onClick={handleManageRoom} iconSrc={HashSearchIC}>
Manage rooms
</MenuItem>
<MenuItem onClick={handleSettingsClick} iconSrc={SettingsIC}>
Settings
</MenuItem>
<MenuItem variant="danger" onClick={handleLeaveClick} iconSrc={LeaveArrowIC}>
Leave
</MenuItem>
</div>

View file

@ -2,7 +2,6 @@ import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import './CreateRoom.scss';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
@ -92,7 +91,7 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
topic,
joinRule,
alias: roomAlias,
isEncrypted: (isSpace || joinRule === 'public') ? false : isEncrypted,
isEncrypted: isSpace || joinRule === 'public' ? false : isEncrypted,
powerLevel,
isSpace,
parentId,
@ -131,36 +130,35 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
const joinRules = ['invite', 'restricted', 'public'];
const joinRuleShortText = ['Private', 'Restricted', 'Public'];
const joinRuleText = ['Private (invite only)', 'Restricted (space member can join)', 'Public (anyone can join)'];
const joinRuleText = [
'Private (invite only)',
'Restricted (space member can join)',
'Public (anyone can join)',
];
const jrRoomIC = [HashLockIC, HashIC, HashGlobeIC];
const jrSpaceIC = [SpaceLockIC, SpaceIC, SpaceGlobeIC];
const handleJoinRule = (evt) => {
openReusableContextMenu(
'bottom',
getEventCords(evt, '.btn-surface'),
(closeMenu) => (
<>
<MenuHeader>Visibility (who can join)</MenuHeader>
{
joinRules.map((rule) => (
<MenuItem
key={rule}
variant={rule === joinRule ? 'positive' : 'surface'}
iconSrc={
isSpace
? jrSpaceIC[joinRules.indexOf(rule)]
: jrRoomIC[joinRules.indexOf(rule)]
}
onClick={() => { closeMenu(); setJoinRule(rule); }}
disabled={!parentId && rule === 'restricted'}
>
{ joinRuleText[joinRules.indexOf(rule)] }
</MenuItem>
))
}
</>
),
);
openReusableContextMenu('bottom', getEventCords(evt, '.btn-surface'), (closeMenu) => (
<>
<MenuHeader>Visibility (who can join)</MenuHeader>
{joinRules.map((rule) => (
<MenuItem
key={rule}
variant={rule === joinRule ? 'positive' : 'surface'}
iconSrc={
isSpace ? jrSpaceIC[joinRules.indexOf(rule)] : jrRoomIC[joinRules.indexOf(rule)]
}
onClick={() => {
closeMenu();
setJoinRule(rule);
}}
disabled={!parentId && rule === 'restricted'}
>
{joinRuleText[joinRules.indexOf(rule)]}
</MenuItem>
))}
</>
));
};
return (
@ -168,50 +166,64 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
<form className="create-room__form" onSubmit={handleSubmit}>
<SettingTile
title="Visibility"
options={(
options={
<Button onClick={handleJoinRule} iconSrc={ChevronBottomIC}>
{joinRuleShortText[joinRules.indexOf(joinRule)]}
</Button>
)}
content={<Text variant="b3">{`Select who can join this ${isSpace ? 'space' : 'room'}.`}</Text>}
}
content={
<Text variant="b3">{`Select who can join this ${isSpace ? 'space' : 'room'}.`}</Text>
}
/>
{joinRule === 'public' && (
<div>
<Text className="create-room__address__label" variant="b2">{isSpace ? 'Space address' : 'Room address'}</Text>
<Text className="create-room__address__label" variant="b2">
{isSpace ? 'Space address' : 'Room address'}
</Text>
<div className="create-room__address">
<Text variant="b1">#</Text>
<Input
value={addressValue}
onChange={validateAddress}
state={(isValidAddress === false) ? 'error' : 'normal'}
state={isValidAddress === false ? 'error' : 'normal'}
forwardRef={addressRef}
placeholder="my_address"
required
/>
<Text variant="b1">{`:${userHs}`}</Text>
</div>
{isValidAddress === false && <Text className="create-room__address__tip" variant="b3"><span style={{ color: 'var(--bg-danger)' }}>{`#${addressValue}:${userHs} is already in use`}</span></Text>}
{isValidAddress === false && (
<Text className="create-room__address__tip" variant="b3">
<span
style={{ color: 'var(--bg-danger)' }}
>{`#${addressValue}:${userHs} is already in use`}</span>
</Text>
)}
</div>
)}
{!isSpace && joinRule !== 'public' && (
<SettingTile
title="Enable end-to-end encryption"
options={<Toggle isActive={isEncrypted} onToggle={setIsEncrypted} />}
content={<Text variant="b3">You cant disable this later. Bridges & most bots wont work yet.</Text>}
content={
<Text variant="b3">
You cant disable this later. Bridges & most bots wont work yet.
</Text>
}
/>
)}
<SettingTile
title="Select your role"
options={(
options={
<SegmentControl
selected={roleIndex}
segments={[{ text: 'Admin' }, { text: 'Founder' }]}
onSelect={setRoleIndex}
/>
)}
content={(
}
content={
<Text variant="b3">Selecting Admin sets 100 power level whereas Founder sets 101.</Text>
)}
}
/>
<Input name="topic" minHeight={174} resizable label="Topic (optional)" />
<div className="create-room__name-wrapper">
@ -231,7 +243,11 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
<Text>{`Creating ${isSpace ? 'space' : 'room'}...`}</Text>
</div>
)}
{typeof creatingError === 'string' && <Text className="create-room__error" variant="b3">{creatingError}</Text>}
{typeof creatingError === 'string' && (
<Text className="create-room__error" variant="b3">
{creatingError}
</Text>
)}
</form>
</div>
);
@ -275,27 +291,22 @@ function CreateRoom() {
return (
<Dialog
isOpen={create !== null}
title={(
title={
<Text variant="s1" weight="medium" primary>
{parentId ? twemojify(room.name) : 'Home'}
{parentId ? room.name : 'Home'}
<span style={{ color: 'var(--tc-surface-low)' }}>
{` — create ${isSpace ? 'space' : 'room'}`}
</span>
</Text>
)}
}
contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
onRequestClose={onRequestClose}
>
{
create
? (
<CreateRoomContent
isSpace={isSpace}
parentId={parentId}
onRequestClose={onRequestClose}
/>
) : <div />
}
{create ? (
<CreateRoomContent isSpace={isSpace} parentId={parentId} onRequestClose={onRequestClose} />
) : (
<div />
)}
</Dialog>
);
}

View file

@ -5,7 +5,6 @@ import PropTypes from 'prop-types';
import './EmojiBoard.scss';
import parse from 'html-react-parser';
import twemoji from 'twemoji';
import { emojiGroups, emojis } from './emoji';
import { getRelevantPacks } from './custom-emoji';
import initMatrix from '../../../client/initMatrix';
@ -13,7 +12,6 @@ import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import AsyncSearch from '../../../util/AsyncSearch';
import { addRecentEmoji, getRecentEmojis } from './recent';
import { TWEMOJI_BASE_URL } from '../../../util/twemojify';
import Text from '../../atoms/text/Text';
import RawIcon from '../../atoms/system-icons/RawIcon';
@ -48,18 +46,7 @@ const EmojiGroup = React.memo(({ name, groupEmojis }) => {
emojiRow.push(
<span key={emojiIndex}>
{emoji.hexcode ? (
// This is a unicode emoji, and should be rendered with twemoji
parse(
twemoji.parse(emoji.unicode, {
attributes: () => ({
unicode: emoji.unicode,
shortcodes: emoji.shortcodes?.toString(),
hexcode: emoji.hexcode,
loading: 'lazy',
}),
base: TWEMOJI_BASE_URL,
})
)
parse(emoji.unicode)
) : (
// This is a custom emoji, and should be render as an mxc
<img
@ -192,7 +179,6 @@ function EmojiBoard({ onSelect, searchRef }) {
setEmojiInfo({
unicode: '🙂',
shortcode: 'slight_smile',
src: 'https://twemoji.maxcdn.com/v/13.1.0/72x72/1f642.png',
});
return;
}
@ -340,7 +326,7 @@ function EmojiBoard({ onSelect, searchRef }) {
</ScrollView>
</div>
<div ref={emojiInfo} className="emoji-board__content__info">
<div>{parse(twemoji.parse('🙂', { base: TWEMOJI_BASE_URL }))}</div>
<div>{parse('🙂')}</div>
<Text>:slight_smile:</Text>
</div>
</div>

View file

@ -2,7 +2,6 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './EmojiVerification.scss';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
@ -30,8 +29,9 @@ function EmojiVerificationContent({ data, requestClose }) {
const beginVerification = async () => {
if (
isCrossVerified(mx.deviceId)
&& (mx.getCrossSigningId() === null || await mx.crypto.crossSigningInfo.isStoredInKeyCache('self_signing') === false)
isCrossVerified(mx.deviceId) &&
(mx.getCrossSigningId() === null ||
(await mx.crypto.crossSigningInfo.isStoredInKeyCache('self_signing')) === false)
) {
if (!hasPrivateKey(getDefaultSSKey())) {
const keyData = await accessSecretStorage('Emoji verification');
@ -106,16 +106,20 @@ function EmojiVerificationContent({ data, requestClose }) {
{sas.sas.emoji.map((emoji, i) => (
// eslint-disable-next-line react/no-array-index-key
<div className="emoji-verification__emoji-block" key={`${emoji[1]}-${i}`}>
<Text variant="h1">{twemojify(emoji[0])}</Text>
<Text variant="h1">{emoji[0]}</Text>
<Text>{emoji[1]}</Text>
</div>
))}
</div>
<div className="emoji-verification__buttons">
{process ? renderWait() : (
{process ? (
renderWait()
) : (
<>
<Button variant="primary" onClick={sasConfirm}>They match</Button>
<Button onClick={sasMismatch}>{'They don\'t match'}</Button>
<Button variant="primary" onClick={sasConfirm}>
They match
</Button>
<Button onClick={sasMismatch}>{"They don't match"}</Button>
</>
)}
</div>
@ -127,9 +131,7 @@ function EmojiVerificationContent({ data, requestClose }) {
return (
<div className="emoji-verification__content">
<Text>Please accept the request from other device.</Text>
<div className="emoji-verification__buttons">
{renderWait()}
</div>
<div className="emoji-verification__buttons">{renderWait()}</div>
</div>
);
}
@ -138,11 +140,13 @@ function EmojiVerificationContent({ data, requestClose }) {
<div className="emoji-verification__content">
<Text>Click accept to start the verification process.</Text>
<div className="emoji-verification__buttons">
{
process
? renderWait()
: <Button variant="primary" onClick={beginVerification}>Accept</Button>
}
{process ? (
renderWait()
) : (
<Button variant="primary" onClick={beginVerification}>
Accept
</Button>
)}
</div>
</div>
);
@ -180,19 +184,19 @@ function EmojiVerification() {
<Dialog
isOpen={data !== null}
className="emoji-verification"
title={(
title={
<Text variant="s1" weight="medium" primary>
Emoji verification
</Text>
)}
}
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
onRequestClose={requestClose}
>
{
data !== null
? <EmojiVerificationContent data={data} requestClose={requestClose} />
: <div />
}
{data !== null ? (
<EmojiVerificationContent data={data} requestClose={requestClose} />
) : (
<div />
)}
</Dialog>
);
}

View file

@ -2,8 +2,6 @@ import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import './DrawerBreadcrumb.scss';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import { selectTab, selectSpace } from '../../../client/action/navigation';
@ -49,8 +47,9 @@ function DrawerBreadcrumb({ spaceId }) {
}, [spaceId]);
function getHomeNotiExcept(childId) {
const orphans = roomList.getOrphans()
.filter((id) => (id !== childId))
const orphans = roomList
.getOrphans()
.filter((id) => id !== childId)
.filter((id) => !accountData.spaceShortcut.has(id));
let noti = null;
@ -94,36 +93,35 @@ function DrawerBreadcrumb({ spaceId }) {
<div className="drawer-breadcrumb__wrapper">
<ScrollView ref={scrollRef} horizontal vertical={false} invisible>
<div className="drawer-breadcrumb">
{
spacePath.map((id, index) => {
const noti = (id !== cons.tabs.HOME && index < spacePath.length)
? getNotiExcept(id, (index === spacePath.length - 1) ? null : spacePath[index + 1])
: getHomeNotiExcept((index === spacePath.length - 1) ? null : spacePath[index + 1]);
{spacePath.map((id, index) => {
const noti =
id !== cons.tabs.HOME && index < spacePath.length
? getNotiExcept(id, index === spacePath.length - 1 ? null : spacePath[index + 1])
: getHomeNotiExcept(index === spacePath.length - 1 ? null : spacePath[index + 1]);
return (
<React.Fragment
key={id}
return (
<React.Fragment key={id}>
{index !== 0 && <RawIcon size="extra-small" src={ChevronRightIC} />}
<Button
className={
index === spacePath.length - 1 ? 'drawer-breadcrumb__btn--selected' : ''
}
onClick={() => {
if (id === cons.tabs.HOME) selectTab(id);
else selectSpace(id);
}}
>
{ index !== 0 && <RawIcon size="extra-small" src={ChevronRightIC} />}
<Button
className={index === spacePath.length - 1 ? 'drawer-breadcrumb__btn--selected' : ''}
onClick={() => {
if (id === cons.tabs.HOME) selectTab(id);
else selectSpace(id);
}}
>
<Text variant="b2">{id === cons.tabs.HOME ? 'Home' : twemojify(mx.getRoom(id).name)}</Text>
{ noti !== null && (
<NotificationBadge
alert={noti.highlight !== 0}
content={noti.total > 0 ? abbreviateNumber(noti.total) : null}
/>
)}
</Button>
</React.Fragment>
);
})
}
<Text variant="b2">{id === cons.tabs.HOME ? 'Home' : mx.getRoom(id).name}</Text>
{noti !== null && (
<NotificationBadge
alert={noti.highlight !== 0}
content={noti.total > 0 ? abbreviateNumber(noti.total) : null}
/>
)}
</Button>
</React.Fragment>
);
})}
<div style={{ width: 'var(--sp-extra-tight)', height: '100%' }} />
</div>
</ScrollView>

View file

@ -2,13 +2,16 @@ import React from 'react';
import PropTypes from 'prop-types';
import './DrawerHeader.scss';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import {
openPublicRooms, openCreateRoom, openSpaceManage, openJoinAlias,
openSpaceAddExisting, openInviteUser, openReusableContextMenu,
openPublicRooms,
openCreateRoom,
openSpaceManage,
openJoinAlias,
openSpaceAddExisting,
openInviteUser,
openReusableContextMenu,
} from '../../../client/action/navigation';
import { getEventCords } from '../../../util/common';
@ -40,46 +43,64 @@ export function HomeSpaceOptions({ spaceId, afterOptionSelect }) {
<MenuHeader>Add rooms or spaces</MenuHeader>
<MenuItem
iconSrc={SpacePlusIC}
onClick={() => { afterOptionSelect(); openCreateRoom(true, spaceId); }}
onClick={() => {
afterOptionSelect();
openCreateRoom(true, spaceId);
}}
disabled={!canManage}
>
Create new space
</MenuItem>
<MenuItem
iconSrc={HashPlusIC}
onClick={() => { afterOptionSelect(); openCreateRoom(false, spaceId); }}
onClick={() => {
afterOptionSelect();
openCreateRoom(false, spaceId);
}}
disabled={!canManage}
>
Create new room
</MenuItem>
{ !spaceId && (
{!spaceId && (
<MenuItem
iconSrc={HashGlobeIC}
onClick={() => { afterOptionSelect(); openPublicRooms(); }}
onClick={() => {
afterOptionSelect();
openPublicRooms();
}}
>
Explore public rooms
</MenuItem>
)}
{ !spaceId && (
{!spaceId && (
<MenuItem
iconSrc={PlusIC}
onClick={() => { afterOptionSelect(); openJoinAlias(); }}
onClick={() => {
afterOptionSelect();
openJoinAlias();
}}
>
Join with address
</MenuItem>
)}
{ spaceId && (
{spaceId && (
<MenuItem
iconSrc={PlusIC}
onClick={() => { afterOptionSelect(); openSpaceAddExisting(spaceId); }}
onClick={() => {
afterOptionSelect();
openSpaceAddExisting(spaceId);
}}
disabled={!canManage}
>
Add existing
</MenuItem>
)}
{ spaceId && (
{spaceId && (
<MenuItem
onClick={() => { afterOptionSelect(); openSpaceManage(spaceId); }}
onClick={() => {
afterOptionSelect();
openSpaceManage(spaceId);
}}
iconSrc={HashSearchIC}
>
Manage rooms
@ -102,24 +123,20 @@ function DrawerHeader({ selectedTab, spaceId }) {
const isDMTab = selectedTab === cons.tabs.DIRECTS;
const room = mx.getRoom(spaceId);
const spaceName = isDMTab ? null : (room?.name || null);
const spaceName = isDMTab ? null : room?.name || null;
const openSpaceOptions = (e) => {
e.preventDefault();
openReusableContextMenu(
'bottom',
getEventCords(e, '.header'),
(closeMenu) => <SpaceOptions roomId={spaceId} afterOptionSelect={closeMenu} />,
);
openReusableContextMenu('bottom', getEventCords(e, '.header'), (closeMenu) => (
<SpaceOptions roomId={spaceId} afterOptionSelect={closeMenu} />
));
};
const openHomeSpaceOptions = (e) => {
e.preventDefault();
openReusableContextMenu(
'right',
getEventCords(e, '.ic-btn'),
(closeMenu) => <HomeSpaceOptions spaceId={spaceId} afterOptionSelect={closeMenu} />,
);
openReusableContextMenu('right', getEventCords(e, '.ic-btn'), (closeMenu) => (
<HomeSpaceOptions spaceId={spaceId} afterOptionSelect={closeMenu} />
));
};
return (
@ -132,18 +149,31 @@ function DrawerHeader({ selectedTab, spaceId }) {
onMouseUp={(e) => blurOnBubbling(e, '.drawer-header__btn')}
>
<TitleWrapper>
<Text variant="s1" weight="medium" primary>{twemojify(spaceName)}</Text>
<Text variant="s1" weight="medium" primary>
{spaceName}
</Text>
</TitleWrapper>
<RawIcon size="small" src={ChevronBottomIC} />
</button>
) : (
<TitleWrapper>
<Text variant="s1" weight="medium" primary>{tabName}</Text>
<Text variant="s1" weight="medium" primary>
{tabName}
</Text>
</TitleWrapper>
)}
{ isDMTab && <IconButton onClick={() => openInviteUser()} tooltip="Start DM" src={PlusIC} size="small" /> }
{ !isDMTab && <IconButton onClick={openHomeSpaceOptions} tooltip="Add rooms/spaces" src={PlusIC} size="small" /> }
{isDMTab && (
<IconButton onClick={() => openInviteUser()} tooltip="Start DM" src={PlusIC} size="small" />
)}
{!isDMTab && (
<IconButton
onClick={openHomeSpaceOptions}
tooltip="Add rooms/spaces"
src={PlusIC}
size="small"
/>
)}
</Header>
);
}

View file

@ -1,6 +1,5 @@
import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import colorMXID from '../../../util/colorMXID';
@ -22,7 +21,9 @@ function ProfileEditor({ userId }) {
const user = mx.getUser(mx.getUserId());
const displayNameRef = useRef(null);
const [avatarSrc, setAvatarSrc] = useState(user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null);
const [avatarSrc, setAvatarSrc] = useState(
user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null
);
const [username, setUsername] = useState(user.displayName);
const [disabled, setDisabled] = useState(true);
@ -44,7 +45,7 @@ function ProfileEditor({ userId }) {
'Remove avatar',
'Are you sure that you want to remove avatar?',
'Remove',
'caution',
'caution'
);
if (isConfirmed) {
mx.setAvatarUrl('');
@ -79,7 +80,10 @@ function ProfileEditor({ userId }) {
<form
className="profile-editor__form"
style={{ marginBottom: avatarSrc ? '24px' : '0' }}
onSubmit={(e) => { e.preventDefault(); saveDisplayName(); }}
onSubmit={(e) => {
e.preventDefault();
saveDisplayName();
}}
>
<Input
label={`Display name of ${mx.getUserId()}`}
@ -87,7 +91,9 @@ function ProfileEditor({ userId }) {
value={mx.getUser(mx.getUserId()).displayName}
forwardRef={displayNameRef}
/>
<Button variant="primary" type="submit" disabled={disabled}>Save</Button>
<Button variant="primary" type="submit" disabled={disabled}>
Save
</Button>
<Button onClick={cancelDisplayNameChanges}>Cancel</Button>
</form>
);
@ -95,7 +101,9 @@ function ProfileEditor({ userId }) {
const renderInfo = () => (
<div className="profile-editor__info" style={{ marginBottom: avatarSrc ? '24px' : '0' }}>
<div>
<Text variant="h2" primary weight="medium">{twemojify(username) ?? userId}</Text>
<Text variant="h2" primary weight="medium">
{username ?? userId}
</Text>
<IconButton
src={PencilIC}
size="extra-small"
@ -116,9 +124,7 @@ function ProfileEditor({ userId }) {
onUpload={handleAvatarUpload}
onRequestRemove={() => handleAvatarUpload(null)}
/>
{
isEditing ? renderForm() : renderInfo()
}
{isEditing ? renderForm() : renderInfo()}
</div>
);
}

View file

@ -2,8 +2,6 @@ import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import './ProfileViewer.scss';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
@ -11,7 +9,11 @@ import { selectRoom, openReusableContextMenu } from '../../../client/action/navi
import * as roomActions from '../../../client/action/room';
import {
getUsername, getUsernameOfRoomMember, getPowerLabel, hasDMWith, hasDevices,
getUsername,
getUsernameOfRoomMember,
getPowerLabel,
hasDMWith,
hasDevices,
} from '../../../util/matrixUtil';
import { getEventCords } from '../../../util/common';
import colorMXID from '../../../util/colorMXID';
@ -34,25 +36,21 @@ import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import { useForceUpdate } from '../../hooks/useForceUpdate';
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
function ModerationTools({
roomId, userId,
}) {
function ModerationTools({ roomId, userId }) {
const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId);
const roomMember = room.getMember(userId);
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0;
const powerLevel = roomMember?.powerLevel || 0;
const canIKick = (
roomMember?.membership === 'join'
&& room.currentState.hasSufficientPowerLevelFor('kick', myPowerLevel)
&& powerLevel < myPowerLevel
);
const canIBan = (
['join', 'leave'].includes(roomMember?.membership)
&& room.currentState.hasSufficientPowerLevelFor('ban', myPowerLevel)
&& powerLevel < myPowerLevel
);
const canIKick =
roomMember?.membership === 'join' &&
room.currentState.hasSufficientPowerLevelFor('kick', myPowerLevel) &&
powerLevel < myPowerLevel;
const canIBan =
['join', 'leave'].includes(roomMember?.membership) &&
room.currentState.hasSufficientPowerLevelFor('ban', myPowerLevel) &&
powerLevel < myPowerLevel;
const handleKick = (e) => {
e.preventDefault();
@ -120,13 +118,14 @@ function SessionInfo({ userId }) {
<div className="session-info__chips">
{devices === null && <Text variant="b2">Loading sessions...</Text>}
{devices?.length === 0 && <Text variant="b2">No session found.</Text>}
{devices !== null && (devices.map((device) => (
<Chip
key={device.deviceId}
iconSrc={ShieldEmptyIC}
text={device.getDisplayName() || device.deviceId}
/>
)))}
{devices !== null &&
devices.map((device) => (
<Chip
key={device.deviceId}
iconSrc={ShieldEmptyIC}
text={device.getDisplayName() || device.deviceId}
/>
))}
</div>
);
}
@ -137,7 +136,9 @@ function SessionInfo({ userId }) {
onClick={() => setIsVisible(!isVisible)}
iconSrc={isVisible ? ChevronBottomIC : ChevronRightIC}
>
<Text variant="b2">{`View ${devices?.length > 0 ? `${devices.length} ` : ''}sessions`}</Text>
<Text variant="b2">{`View ${
devices?.length > 0 ? `${devices.length} ` : ''
}sessions`}</Text>
</MenuItem>
{renderSessionChips()}
</div>
@ -164,7 +165,8 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
const userPL = room.getMember(userId)?.powerLevel || 0;
const canIKick = room.currentState.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel;
const canIKick =
room.currentState.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel;
const isBanned = member?.membership === 'ban';
@ -246,31 +248,19 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
return (
<div className="profile-viewer__buttons">
<Button
variant="primary"
onClick={openDM}
disabled={isCreatingDM}
>
<Button variant="primary" onClick={openDM} disabled={isCreatingDM}>
{isCreatingDM ? 'Creating room...' : 'Message'}
</Button>
{ isBanned && canIKick && (
<Button
variant="positive"
onClick={() => roomActions.unban(roomId, userId)}
>
{isBanned && canIKick && (
<Button variant="positive" onClick={() => roomActions.unban(roomId, userId)}>
Unban
</Button>
)}
{ (isInvited ? canIKick : room.canInvite(mx.getUserId())) && isInvitable && (
<Button
onClick={toggleInvite}
disabled={isInviting}
>
{
isInvited
? `${isInviting ? 'Disinviting...' : 'Disinvite'}`
: `${isInviting ? 'Inviting...' : 'Invite'}`
}
{(isInvited ? canIKick : room.canInvite(mx.getUserId())) && isInvitable && (
<Button onClick={toggleInvite} disabled={isInviting}>
{isInvited
? `${isInviting ? 'Disinviting...' : 'Disinvite'}`
: `${isInviting ? 'Inviting...' : 'Invite'}`}
</Button>
)}
<Button
@ -278,11 +268,9 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
onClick={toggleIgnore}
disabled={isIgnoring}
>
{
isUserIgnored
? `${isIgnoring ? 'Unignoring...' : 'Unignore'}`
: `${isIgnoring ? 'Ignoring...' : 'Ignore'}`
}
{isUserIgnored
? `${isIgnoring ? 'Unignoring...' : 'Unignore'}`
: `${isIgnoring ? 'Ignoring...' : 'Ignore'}`}
</Button>
</div>
);
@ -326,8 +314,8 @@ function useRerenderOnProfileChange(roomId, userId) {
useEffect(() => {
const handleProfileChange = (mEvent, member) => {
if (
mEvent.getRoomId() === roomId
&& (member.userId === userId || member.userId === mx.getUserId())
mEvent.getRoomId() === roomId &&
(member.userId === userId || member.userId === mx.getUserId())
) {
forceUpdate();
}
@ -352,20 +340,22 @@ function ProfileViewer() {
const roomMember = room.getMember(userId);
const username = roomMember ? getUsernameOfRoomMember(roomMember) : getUsername(userId);
const avatarMxc = roomMember?.getMxcAvatarUrl?.() || mx.getUser(userId)?.avatarUrl;
const avatarUrl = (avatarMxc && avatarMxc !== 'null') ? mx.mxcUrlToHttp(avatarMxc, 80, 80, 'crop') : null;
const avatarUrl =
avatarMxc && avatarMxc !== 'null' ? mx.mxcUrlToHttp(avatarMxc, 80, 80, 'crop') : null;
const powerLevel = roomMember?.powerLevel || 0;
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0;
const canChangeRole = (
room.currentState.maySendEvent('m.room.power_levels', mx.getUserId())
&& (powerLevel < myPowerLevel || userId === mx.getUserId())
);
const canChangeRole =
room.currentState.maySendEvent('m.room.power_levels', mx.getUserId()) &&
(powerLevel < myPowerLevel || userId === mx.getUserId());
const handleChangePowerLevel = async (newPowerLevel) => {
if (newPowerLevel === powerLevel) return;
const SHARED_POWER_MSG = 'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?';
const DEMOTING_MYSELF_MSG = 'You will not be able to undo this change as you are demoting yourself. Are you sure?';
const SHARED_POWER_MSG =
'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?';
const DEMOTING_MYSELF_MSG =
'You will not be able to undo this change as you are demoting yourself. Are you sure?';
const isSharedPower = newPowerLevel === myPowerLevel;
const isDemotingMyself = userId === mx.getUserId();
@ -374,7 +364,7 @@ function ProfileViewer() {
'Change power level',
isSharedPower ? SHARED_POWER_MSG : DEMOTING_MYSELF_MSG,
'Change',
'caution',
'caution'
);
if (!isConfirmed) return;
roomActions.setPowerLevel(roomId, userId, newPowerLevel);
@ -384,20 +374,16 @@ function ProfileViewer() {
};
const handlePowerSelector = (e) => {
openReusableContextMenu(
'bottom',
getEventCords(e, '.btn-surface'),
(closeMenu) => (
<PowerLevelSelector
value={powerLevel}
max={myPowerLevel}
onSelect={(pl) => {
closeMenu();
handleChangePowerLevel(pl);
}}
/>
),
);
openReusableContextMenu('bottom', getEventCords(e, '.btn-surface'), (closeMenu) => (
<PowerLevelSelector
value={powerLevel}
max={myPowerLevel}
onSelect={(pl) => {
closeMenu();
handleChangePowerLevel(pl);
}}
/>
));
};
return (
@ -405,8 +391,10 @@ function ProfileViewer() {
<div className="profile-viewer__user">
<Avatar imageSrc={avatarUrl} text={username} bgColor={colorMXID(userId)} size="large" />
<div className="profile-viewer__user__info">
<Text variant="s1" weight="medium">{twemojify(username)}</Text>
<Text variant="b2">{twemojify(userId)}</Text>
<Text variant="s1" weight="medium">
{username}
</Text>
<Text variant="b2">{userId}</Text>
</div>
<div className="profile-viewer__user__role">
<Text variant="b3">Role</Text>
@ -420,7 +408,7 @@ function ProfileViewer() {
</div>
<ModerationTools roomId={roomId} userId={userId} />
<SessionInfo userId={userId} />
{ userId !== mx.getUserId() && (
{userId !== mx.getUserId() && (
<ProfileFooter roomId={roomId} userId={userId} onRequestClose={closeDialog} />
)}
</div>

View file

@ -48,6 +48,11 @@ function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
function renderEmojiSuggestion(emPrefix, emos) {
const mx = initMatrix.matrixClient;
// Renders a small Twemoji
function renderTwemoji(emoji) {
return parse(emoji.unicode);
}
// Render a custom emoji
function renderCustomEmoji(emoji) {
return (
@ -60,9 +65,12 @@ function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
);
}
// Dynamically render either a custom emoji
// Dynamically render either a custom emoji or twemoji based on what the input is
function renderEmoji(emoji) {
return renderCustomEmoji(emoji);
if (emoji.mxc) {
return renderCustomEmoji(emoji);
}
return renderTwemoji(emoji);
}
return emos.map((emoji) => (
@ -92,7 +100,7 @@ function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
});
}}
>
<Text variant="b2">{}</Text>
<Text variant="b2">{twemojify(member.name)}</Text>
</CmdItem>
));
}

View file

@ -88,7 +88,7 @@ function RoomIntroContainer({ event, timeline }) {
roomId={timeline.roomId}
avatarSrc={avatarSrc}
name={room.name}
heading={twemojify(heading)}
heading={heading}
desc={desc}
time={event ? `Created at ${dateFormat(event.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null}
/>

View file

@ -2,13 +2,16 @@ import React, { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import './RoomViewHeader.scss';
import { twemojify } from '../../../util/twemojify';
import { blurOnBubbling } from '../../atoms/button/script';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import { toggleRoomSettings, openReusableContextMenu, openNavigation } from '../../../client/action/navigation';
import {
toggleRoomSettings,
openReusableContextMenu,
openNavigation,
} from '../../../client/action/navigation';
import { togglePeopleDrawer } from '../../../client/action/settings';
import colorMXID from '../../../util/colorMXID';
import { getEventCords } from '../../../util/common';
@ -35,16 +38,16 @@ function RoomViewHeader({ roomId }) {
const isDM = initMatrix.roomList.directs.has(roomId);
const room = mx.getRoom(roomId);
let avatarSrc = room.getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
avatarSrc = isDM ? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop') : avatarSrc;
avatarSrc = isDM
? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop')
: avatarSrc;
const roomName = room.name;
const roomHeaderBtnRef = useRef(null);
useEffect(() => {
const settingsToggle = (isVisibile) => {
const rawIcon = roomHeaderBtnRef.current.lastElementChild;
rawIcon.style.transform = isVisibile
? 'rotateX(180deg)'
: 'rotateX(0deg)';
rawIcon.style.transform = isVisibile ? 'rotateX(180deg)' : 'rotateX(0deg)';
};
navigation.on(cons.events.navigation.ROOM_SETTINGS_TOGGLED, settingsToggle);
return () => {
@ -66,11 +69,9 @@ function RoomViewHeader({ roomId }) {
}, [roomId]);
const openRoomOptions = (e) => {
openReusableContextMenu(
'bottom',
getEventCords(e, '.ic-btn'),
(closeMenu) => <RoomOptions roomId={roomId} afterOptionSelect={closeMenu} />,
);
openReusableContextMenu('bottom', getEventCords(e, '.ic-btn'), (closeMenu) => (
<RoomOptions roomId={roomId} afterOptionSelect={closeMenu} />
));
};
return (
@ -90,18 +91,32 @@ function RoomViewHeader({ roomId }) {
>
<Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="small" />
<TitleWrapper>
<Text variant="h2" weight="medium" primary>{twemojify(roomName)}</Text>
<Text variant="h2" weight="medium" primary>
{roomName}
</Text>
</TitleWrapper>
<RawIcon src={ChevronBottomIC} />
</button>
{mx.isRoomEncrypted(roomId) === false && <IconButton onClick={() => toggleRoomSettings(tabText.SEARCH)} tooltip="Search" src={SearchIC} />}
<IconButton className="room-header__drawer-btn" onClick={togglePeopleDrawer} tooltip="People" src={UserIC} />
<IconButton className="room-header__members-btn" onClick={() => toggleRoomSettings(tabText.MEMBERS)} tooltip="Members" src={UserIC} />
{mx.isRoomEncrypted(roomId) === false && (
<IconButton
onClick={() => toggleRoomSettings(tabText.SEARCH)}
tooltip="Search"
src={SearchIC}
/>
)}
<IconButton
onClick={openRoomOptions}
tooltip="Options"
src={VerticalMenuIC}
className="room-header__drawer-btn"
onClick={togglePeopleDrawer}
tooltip="People"
src={UserIC}
/>
<IconButton
className="room-header__members-btn"
onClick={() => toggleRoomSettings(tabText.MEMBERS)}
tooltip="Members"
src={UserIC}
/>
<IconButton onClick={openRoomOptions} tooltip="Options" src={VerticalMenuIC} />
</Header>
);
}

View file

@ -2,7 +2,6 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './KeyBackup.scss';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import { openReusableDialog } from '../../../client/action/navigation';
@ -34,10 +33,7 @@ function CreateKeyBackupDialog({ keyData }) {
let info;
try {
info = await mx.prepareKeyBackupVersion(
null,
{ secureSecretStorage: true },
);
info = await mx.prepareKeyBackupVersion(null, { secureSecretStorage: true });
info = await mx.createKeyBackupVersion(info);
await mx.scheduleAllGroupSessionsForBackup();
if (!mountStore.getItem()) return;
@ -65,7 +61,7 @@ function CreateKeyBackupDialog({ keyData }) {
)}
{done === true && (
<>
<Text variant="h1">{twemojify('✅')}</Text>
<Text variant="h1">'✅'</Text>
<Text>Successfully created backup</Text>
</>
)}
@ -104,12 +100,9 @@ function RestoreKeyBackupDialog({ keyData }) {
try {
const backupInfo = await mx.getKeyBackupVersion();
const info = await mx.restoreKeyBackupWithSecretStorage(
backupInfo,
undefined,
undefined,
{ progressCallback },
);
const info = await mx.restoreKeyBackupWithSecretStorage(backupInfo, undefined, undefined, {
progressCallback,
});
if (!mountStore.getItem()) return;
setStatus({ done: `Successfully restored backup keys (${info.imported}/${info.total}).` });
} catch (e) {
@ -138,7 +131,7 @@ function RestoreKeyBackupDialog({ keyData }) {
)}
{status.done && (
<>
<Text variant="h1">{twemojify('✅')}</Text>
<Text variant="h1">'✅'</Text>
<Text>{status.done}</Text>
</>
)}
@ -176,14 +169,16 @@ function DeleteKeyBackupDialog({ requestClose }) {
return (
<div className="key-backup__delete">
<Text variant="h1">{twemojify('🗑')}</Text>
<Text variant="h1">{'🗑'}</Text>
<Text weight="medium">Deleting key backup is permanent.</Text>
<Text>All encrypted messages keys stored on server will be deleted.</Text>
{
isDeleting
? <Spinner size="small" />
: <Button variant="danger" onClick={deleteBackup}>Delete</Button>
}
{isDeleting ? (
<Spinner size="small" />
) : (
<Button variant="danger" onClick={deleteBackup}>
Delete
</Button>
)}
</div>
);
}
@ -224,9 +219,11 @@ function KeyBackup() {
if (keyData === null) return;
openReusableDialog(
<Text variant="s1" weight="medium">Create Key Backup</Text>,
<Text variant="s1" weight="medium">
Create Key Backup
</Text>,
() => <CreateKeyBackupDialog keyData={keyData} />,
() => fetchKeyBackupVersion(),
() => fetchKeyBackupVersion()
);
};
@ -235,29 +232,44 @@ function KeyBackup() {
if (keyData === null) return;
openReusableDialog(
<Text variant="s1" weight="medium">Restore Key Backup</Text>,
() => <RestoreKeyBackupDialog keyData={keyData} />,
<Text variant="s1" weight="medium">
Restore Key Backup
</Text>,
() => <RestoreKeyBackupDialog keyData={keyData} />
);
};
const openDeleteKeyBackup = () => openReusableDialog(
<Text variant="s1" weight="medium">Delete Key Backup</Text>,
(requestClose) => (
<DeleteKeyBackupDialog
requestClose={(isDone) => {
if (isDone) setKeyBackup(null);
requestClose();
}}
/>
),
);
const openDeleteKeyBackup = () =>
openReusableDialog(
<Text variant="s1" weight="medium">
Delete Key Backup
</Text>,
(requestClose) => (
<DeleteKeyBackupDialog
requestClose={(isDone) => {
if (isDone) setKeyBackup(null);
requestClose();
}}
/>
)
);
const renderOptions = () => {
if (keyBackup === undefined) return <Spinner size="small" />;
if (keyBackup === null) return <Button variant="primary" onClick={openCreateKeyBackup}>Create Backup</Button>;
if (keyBackup === null)
return (
<Button variant="primary" onClick={openCreateKeyBackup}>
Create Backup
</Button>
);
return (
<>
<IconButton src={DownloadIC} variant="positive" onClick={openRestoreKeyBackup} tooltip="Restore backup" />
<IconButton
src={DownloadIC}
variant="positive"
onClick={openRestoreKeyBackup}
tooltip="Restore backup"
/>
<IconButton src={BinIC} onClick={openDeleteKeyBackup} tooltip="Delete backup" />
</>
);
@ -266,9 +278,12 @@ function KeyBackup() {
return (
<SettingTile
title="Encrypted messages backup"
content={(
content={
<>
<Text variant="b3">Online backup your encrypted messages keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.</Text>
<Text variant="b3">
Online backup your encrypted messages keys with your account data in case you lose
access to your sessions. Your keys will be secured with a unique Security Key.
</Text>
{!isCSEnabled && (
<InfoCard
style={{ marginTop: 'var(--sp-ultra-tight)' }}
@ -279,7 +294,7 @@ function KeyBackup() {
/>
)}
</>
)}
}
options={isCSEnabled ? renderOptions() : null}
/>
);

View file

@ -3,8 +3,6 @@ import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './SpaceManage.scss';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
@ -37,32 +35,37 @@ function SpaceManageBreadcrumb({ path, onSelect }) {
<div className="space-manage-breadcrumb__wrapper">
<ScrollView horizontal vertical={false} invisible>
<div className="space-manage-breadcrumb">
{
path.map((item, index) => (
<React.Fragment key={item.roomId}>
{index > 0 && <RawIcon size="extra-small" src={ChevronRightIC} />}
<Button onClick={() => onSelect(item.roomId, item.name)}>
<Text variant="b2">{twemojify(item.name)}</Text>
</Button>
</React.Fragment>
))
}
{path.map((item, index) => (
<React.Fragment key={item.roomId}>
{index > 0 && <RawIcon size="extra-small" src={ChevronRightIC} />}
<Button onClick={() => onSelect(item.roomId, item.name)}>
<Text variant="b2">{item.name}</Text>
</Button>
</React.Fragment>
))}
</div>
</ScrollView>
</div>
);
}
SpaceManageBreadcrumb.propTypes = {
path: PropTypes.arrayOf(PropTypes.exact({
roomId: PropTypes.string,
name: PropTypes.string,
})).isRequired,
path: PropTypes.arrayOf(
PropTypes.exact({
roomId: PropTypes.string,
name: PropTypes.string,
})
).isRequired,
onSelect: PropTypes.func.isRequired,
};
function SpaceManageItem({
parentId, roomInfo, onSpaceClick, requestClose,
isSelected, onSelect, roomHierarchy,
parentId,
roomInfo,
onSpaceClick,
requestClose,
isSelected,
onSelect,
roomHierarchy,
}) {
const [isExpand, setIsExpand] = useState(false);
const [isJoining, setIsJoining] = useState(false);
@ -72,8 +75,11 @@ function SpaceManageItem({
const parentRoom = mx.getRoom(parentId);
const isSpace = roomInfo.room_type === 'm.space';
const roomId = roomInfo.room_id;
const canManage = parentRoom?.currentState.maySendStateEvent('m.space.child', mx.getUserId()) || false;
const isSuggested = parentRoom?.currentState.getStateEvents('m.space.child', roomId)?.getContent().suggested === true;
const canManage =
parentRoom?.currentState.maySendStateEvent('m.space.child', mx.getUserId()) || false;
const isSuggested =
parentRoom?.currentState.getStateEvents('m.space.child', roomId)?.getContent().suggested ===
true;
const room = mx.getRoom(roomId);
const isJoined = !!(room?.getMyMembership() === 'join' || null);
@ -103,17 +109,13 @@ function SpaceManageItem({
bgColor={colorMXID(roomId)}
imageSrc={isDM ? imageSrc : null}
iconColor="var(--ic-surface-low)"
iconSrc={
isDM
? null
: joinRuleToIconSrc((roomInfo.join_rules || roomInfo.join_rule), isSpace)
}
iconSrc={isDM ? null : joinRuleToIconSrc(roomInfo.join_rules || roomInfo.join_rule, isSpace)}
size="extra-small"
/>
);
const roomNameJSX = (
<Text>
{twemojify(name)}
{name}
<Text variant="b3" span>{`${roomInfo.num_joined_members} members`}</Text>
</Text>
);
@ -130,11 +132,11 @@ function SpaceManageItem({
);
return (
<div
className={`space-manage-item${isSpace ? '--space' : ''}`}
>
<div className={`space-manage-item${isSpace ? '--space' : ''}`}>
<div>
{canManage && <Checkbox isActive={isSelected} onToggle={() => onSelect(roomId)} variant="positive" />}
{canManage && (
<Checkbox isActive={isSelected} onToggle={() => onSelect(roomId)} variant="positive" />
)}
<button
className="space-manage-item__btn"
onClick={isSpace ? () => onSpaceClick(roomId, name) : null}
@ -145,13 +147,15 @@ function SpaceManageItem({
{isSuggested && <Text variant="b2">Suggested</Text>}
</button>
{roomInfo.topic && expandBtnJsx}
{
isJoined
? <Button onClick={handleOpen}>Open</Button>
: <Button variant="primary" onClick={handleJoin} disabled={isJoining}>{isJoining ? 'Joining...' : 'Join'}</Button>
}
{isJoined ? (
<Button onClick={handleOpen}>Open</Button>
) : (
<Button variant="primary" onClick={handleJoin} disabled={isJoining}>
{isJoining ? 'Joining...' : 'Join'}
</Button>
)}
</div>
{isExpand && roomInfo.topic && <Text variant="b2">{twemojify(roomInfo.topic, undefined, true)}</Text>}
{isExpand && roomInfo.topic && <Text variant="b2">{roomInfo.topic}</Text>}
</div>
);
}
@ -201,9 +205,11 @@ function SpaceManageFooter({ parentId, selected }) {
<div className="space-manage__footer">
{process && <Spinner size="small" />}
<Text weight="medium">{process || `${selected.length} item selected`}</Text>
{ !process && (
{!process && (
<>
<Button onClick={handleRemove} variant="danger">Remove</Button>
<Button onClick={handleRemove} variant="danger">
Remove
</Button>
<Button
onClick={() => handleToggleSuggested(!allSuggested)}
variant={allSuggested ? 'surface' : 'primary'}
@ -336,20 +342,19 @@ function SpaceManageContent({ roomId, requestClose }) {
if (!currentHierarchy) loadRoomHierarchy();
return (
<div className="space-manage__content">
{spacePath.length > 1 && (
<SpaceManageBreadcrumb path={spacePath} onSelect={addPathItem} />
)}
<Text variant="b3" weight="bold">Rooms and spaces</Text>
{spacePath.length > 1 && <SpaceManageBreadcrumb path={spacePath} onSelect={addPathItem} />}
<Text variant="b3" weight="bold">
Rooms and spaces
</Text>
<div className="space-manage__content-items">
{!isLoading && currentHierarchy?.rooms?.length === 1 && (
<Text>
Either the space contains private rooms or you need to join space to view it's rooms.
</Text>
)}
{currentHierarchy && (currentHierarchy.rooms?.map((roomInfo) => (
roomInfo.room_id === currentPath.roomId
? null
: (
{currentHierarchy &&
currentHierarchy.rooms?.map((roomInfo) =>
roomInfo.room_id === currentPath.roomId ? null : (
<SpaceManageItem
key={roomInfo.room_id}
isSelected={selected.includes(roomInfo.room_id)}
@ -361,7 +366,7 @@ function SpaceManageContent({ roomId, requestClose }) {
onSelect={handleSelected}
/>
)
)))}
)}
{!currentHierarchy && <Text>loading...</Text>}
</div>
{currentHierarchy?.canLoadMore && !isLoading && (
@ -410,20 +415,16 @@ function SpaceManage() {
<PopupWindow
isOpen={roomId !== null}
className="space-manage"
title={(
title={
<Text variant="s1" weight="medium" primary>
{roomId && twemojify(room.name)}
{roomId && room.name}
<span style={{ color: 'var(--tc-surface-low)' }}> manage rooms</span>
</Text>
)}
}
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
onRequestClose={requestClose}
>
{
roomId
? <SpaceManageContent roomId={roomId} requestClose={requestClose} />
: <div />
}
{roomId ? <SpaceManageContent roomId={roomId} requestClose={requestClose} /> : <div />}
</PopupWindow>
);
}

View file

@ -2,8 +2,6 @@ import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './SpaceSettings.scss';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
@ -48,23 +46,28 @@ const tabText = {
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,
}];
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 isPinned = initMatrix.accountData.spaceShortcut.has(roomId);
@ -103,7 +106,7 @@ function GeneralSettings({ roomId }) {
'Leave space',
`Are you sure that you want to leave "${roomName}" space?`,
'Leave',
'danger',
'danger'
);
if (isConfirmed) leave(roomId);
}}
@ -165,12 +168,12 @@ function SpaceSettings() {
<PopupWindow
isOpen={isOpen}
className="space-settings"
title={(
title={
<Text variant="s1" weight="medium" primary>
{isOpen && twemojify(room.name)}
{isOpen && room.name}
<span style={{ color: 'var(--tc-surface-low)' }}> space settings</span>
</Text>
)}
}
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
onRequestClose={requestClose}
>