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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,8 +2,6 @@ import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import './RoomProfile.scss'; import './RoomProfile.scss';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons'; import cons from '../../../client/state/cons';
import colorMXID from '../../../util/colorMXID'; import colorMXID from '../../../util/colorMXID';
@ -33,7 +31,9 @@ function RoomProfile({ roomId }) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const isDM = initMatrix.roomList.directs.has(roomId); const isDM = initMatrix.roomList.directs.has(roomId);
let avatarSrc = mx.getRoom(roomId).getAvatarUrl(mx.baseUrl, 36, 36, 'crop'); 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 room = mx.getRoom(roomId);
const { currentState } = room; const { currentState } = room;
const roomName = room.name; const roomName = room.name;
@ -122,7 +122,7 @@ function RoomProfile({ roomId }) {
'Remove avatar', 'Remove avatar',
'Are you sure that you want to remove room avatar?', 'Are you sure that you want to remove room avatar?',
'Remove', 'Remove',
'caution', 'caution'
); );
if (isConfirmed) { if (isConfirmed) {
await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, ''); await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
@ -132,15 +132,45 @@ function RoomProfile({ roomId }) {
const renderEditNameAndTopic = () => ( const renderEditNameAndTopic = () => (
<form className="room-profile__edit-form" onSubmit={handleOnSubmit}> <form className="room-profile__edit-form" onSubmit={handleOnSubmit}>
{canChangeName && <Input value={roomName} name="room-name" disabled={status.type === cons.status.IN_FLIGHT} label="Name" />} {canChangeName && (
{canChangeTopic && <Input value={roomTopic} name="room-topic" disabled={status.type === cons.status.IN_FLIGHT} minHeight={100} resizable label="Topic" />} <Input
{(!canChangeName || !canChangeTopic) && <Text variant="b3">{`You have permission to change ${room.isSpaceRoom() ? 'space' : 'room'} ${canChangeName ? 'name' : 'topic'} only.`}</Text>} value={roomName}
{ status.type === cons.status.IN_FLIGHT && <Text variant="b2">{status.msg}</Text>} name="room-name"
{ status.type === cons.status.SUCCESS && <Text style={{ color: 'var(--tc-positive-high)' }} variant="b2">{status.msg}</Text>} disabled={status.type === cons.status.IN_FLIGHT}
{ status.type === cons.status.ERROR && <Text style={{ color: 'var(--tc-danger-high)' }} variant="b2">{status.msg}</Text>} label="Name"
{ status.type !== cons.status.IN_FLIGHT && ( />
)}
{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> <div>
<Button type="submit" variant="primary">Save</Button> <Button type="submit" variant="primary">
Save
</Button>
<Button onClick={handleCancelEditing}>Cancel</Button> <Button onClick={handleCancelEditing}>Cancel</Button>
</div> </div>
)} )}
@ -148,10 +178,15 @@ function RoomProfile({ roomId }) {
); );
const renderNameAndTopic = () => ( 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> <div>
<Text variant="h2" weight="medium" primary>{twemojify(roomName)}</Text> <Text variant="h2" weight="medium" primary>
{ (canChangeName || canChangeTopic) && ( {roomName}
</Text>
{(canChangeName || canChangeTopic) && (
<IconButton <IconButton
src={PencilIC} src={PencilIC}
size="extra-small" size="extra-small"
@ -161,15 +196,17 @@ function RoomProfile({ roomId }) {
)} )}
</div> </div>
<Text variant="b3">{room.getCanonicalAlias() || room.roomId}</Text> <Text variant="b3">{room.getCanonicalAlias() || room.roomId}</Text>
{roomTopic && <Text variant="b2">{twemojify(roomTopic, undefined, true)}</Text>} {roomTopic && <Text variant="b2">{roomTopic}</Text>}
</div> </div>
); );
return ( return (
<div className="room-profile"> <div className="room-profile">
<div className="room-profile__content"> <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 <ImageUpload
text={roomName} text={roomName}
bgColor={colorMXID(roomId)} bgColor={colorMXID(roomId)}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -48,6 +48,11 @@ function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
function renderEmojiSuggestion(emPrefix, emos) { function renderEmojiSuggestion(emPrefix, emos) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
// Renders a small Twemoji
function renderTwemoji(emoji) {
return parse(emoji.unicode);
}
// Render a custom emoji // Render a custom emoji
function renderCustomEmoji(emoji) { function renderCustomEmoji(emoji) {
return ( 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) { function renderEmoji(emoji) {
return renderCustomEmoji(emoji); if (emoji.mxc) {
return renderCustomEmoji(emoji);
}
return renderTwemoji(emoji);
} }
return emos.map((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> </CmdItem>
)); ));
} }

View file

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

View file

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

View file

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

View file

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