Compare commits

...

6 commits

Author SHA1 Message Date
Array in a Matrix 0aacd4f75b
Merge pull request #4 from array-in-a-matrix/no-twemoji
Remove twemojies
2023-04-03 21:47:14 -07:00
array-in-a-matrix b1aceec240 remove twemoji 2023-04-04 00:30:35 -04:00
array-in-a-matrix 6da274d1a9 remove twemoji 2023-04-04 00:13:32 -04:00
array-in-a-matrix 5c5acbd763 remove twemoji 2023-04-03 22:59:00 -04:00
array-in-a-matrix d6c8036a81 remove twemoji from CmdBar 2023-04-03 21:49:38 -04:00
array-in-a-matrix 57fc0414ab remove some twemoji functions 2023-04-03 15:16:20 -04:00
28 changed files with 1125 additions and 1007 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,46 +2,35 @@ import React from 'react';
import PropTypes from 'prop-types';
import './RoomTile.scss';
import { twemojify } from '../../../util/twemojify';
import colorMXID from '../../../util/colorMXID';
import Text from '../../atoms/text/Text';
import Avatar from '../../atoms/avatar/Avatar';
function RoomTile({
avatarSrc, name, id,
inviterName, memberCount, desc, options,
}) {
function RoomTile({ avatarSrc, name, id, inviterName, memberCount, desc, options }) {
return (
<div className="room-tile">
<div className="room-tile__avatar">
<Avatar
imageSrc={avatarSrc}
bgColor={colorMXID(id)}
text={name}
/>
<Avatar imageSrc={avatarSrc} bgColor={colorMXID(id)} text={name} />
</div>
<div className="room-tile__content">
<Text variant="s1">{twemojify(name)}</Text>
<Text variant="s1">{name}</Text>
<Text variant="b3">
{
inviterName !== null
? `Invited by ${inviterName} to ${id}${memberCount === null ? '' : `${memberCount} members`}`
: id + (memberCount === null ? '' : `${memberCount} members`)
}
{inviterName !== null
? `Invited by ${inviterName} to ${id}${
memberCount === null ? '' : `${memberCount} members`
}`
: id + (memberCount === null ? '' : `${memberCount} members`)}
</Text>
{
desc !== null && (typeof desc === 'string')
? <Text className="room-tile__content__desc" variant="b2">{twemojify(desc, undefined, true)}</Text>
: desc
}
{desc !== null && typeof desc === 'string' ? (
<Text className="room-tile__content__desc" variant="b2">
{desc}
</Text>
) : (
desc
)}
</div>
{ options !== null && (
<div className="room-tile__options">
{options}
</div>
)}
{options !== null && <div className="room-tile__options">{options}</div>}
</div>
);
}
@ -58,10 +47,7 @@ RoomTile.propTypes = {
name: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
inviterName: PropTypes.string,
memberCount: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
memberCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
desc: PropTypes.node,
options: PropTypes.node,
};

View file

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

View file

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

View file

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

View file

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

View file

@ -55,7 +55,6 @@ const EmojiGroup = React.memo(({ name, groupEmojis }) => {
unicode: emoji.unicode,
shortcodes: emoji.shortcodes?.toString(),
hexcode: emoji.hexcode,
loading: 'lazy',
}),
base: TWEMOJI_BASE_URL,
})
@ -340,7 +339,7 @@ function EmojiBoard({ onSelect, searchRef }) {
</ScrollView>
</div>
<div ref={emojiInfo} className="emoji-board__content__info">
<div>{parse(twemoji.parse('🙂', { base: TWEMOJI_BASE_URL }))}</div>
<div>{parse('🙂')}</div>
<Text>:slight_smile:</Text>
</div>
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,9 +3,6 @@ import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './RoomViewCmdBar.scss';
import parse from 'html-react-parser';
import twemoji from 'twemoji';
import { twemojify, TWEMOJI_BASE_URL } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import { getEmojiForCompletion } from '../emoji-board/custom-emoji';
@ -51,19 +48,6 @@ function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
function renderEmojiSuggestion(emPrefix, emos) {
const mx = initMatrix.matrixClient;
// Renders a small Twemoji
function renderTwemoji(emoji) {
return parse(
twemoji.parse(emoji.unicode, {
attributes: () => ({
unicode: emoji.unicode,
shortcodes: emoji.shortcodes?.toString(),
}),
base: TWEMOJI_BASE_URL,
})
);
}
// Render a custom emoji
function renderCustomEmoji(emoji) {
return (
@ -76,12 +60,12 @@ function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
);
}
// Dynamically render either a custom emoji or twemoji based on what the input is
// Dynamically render either a custom emoji or emoji based on what the input is
function renderEmoji(emoji) {
if (emoji.mxc) {
return renderCustomEmoji(emoji);
}
return renderTwemoji(emoji);
return parse(emoji.unicode);
}
return emos.map((emoji) => (
@ -111,7 +95,7 @@ function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
});
}}
>
<Text variant="b2">{twemojify(member.name)}</Text>
<Text variant="b2">{member.name}</Text>
</CmdItem>
));
}

View file

@ -1,14 +1,11 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable react/prop-types */
import React, {
useState, useEffect, useLayoutEffect, useCallback, useRef,
} from 'react';
import React, { useState, useEffect, useLayoutEffect, useCallback, useRef } from 'react';
import PropTypes from 'prop-types';
import './RoomViewContent.scss';
import dateFormat from 'dateformat';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
@ -44,11 +41,7 @@ function loadingMsgPlaceholders(key, count = 2) {
return pl;
};
return (
<React.Fragment key={`placeholder-container${key}`}>
{genPlaceholders()}
</React.Fragment>
);
return <React.Fragment key={`placeholder-container${key}`}>{genPlaceholders()}</React.Fragment>;
}
function RoomIntroContainer({ event, timeline }) {
@ -59,28 +52,27 @@ function RoomIntroContainer({ event, timeline }) {
const roomTopic = room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
const isDM = roomList.directs.has(timeline.roomId);
let avatarSrc = room.getAvatarUrl(mx.baseUrl, 80, 80, 'crop');
avatarSrc = isDM ? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 80, 80, 'crop') : avatarSrc;
avatarSrc = isDM
? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 80, 80, 'crop')
: avatarSrc;
const heading = isDM ? room.name : `Welcome to ${room.name}`;
const topic = twemojify(roomTopic || '', undefined, true);
const nameJsx = twemojify(room.name);
const desc = isDM
? (
<>
This is the beginning of your direct message history with @
<b>{nameJsx}</b>
{'. '}
{topic}
</>
)
: (
<>
{'This is the beginning of the '}
<b>{nameJsx}</b>
{' room. '}
{topic}
</>
);
const topic = roomTopic || '';
const nameJsx = room.name;
const desc = isDM ? (
<>
This is the beginning of your direct message history with @<b>{nameJsx}</b>
{'. '}
{topic}
</>
) : (
<>
{'This is the beginning of the '}
<b>{nameJsx}</b>
{' room. '}
{topic}
</>
);
useEffect(() => {
const handleUpdate = () => nameForceUpdate();
@ -96,7 +88,7 @@ function RoomIntroContainer({ event, timeline }) {
roomId={timeline.roomId}
avatarSrc={avatarSrc}
name={room.name}
heading={twemojify(heading)}
heading={heading}
desc={desc}
time={event ? `Created at ${dateFormat(event.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null}
/>
@ -118,21 +110,13 @@ function handleOnClickCapture(e) {
}
}
function renderEvent(
roomTimeline,
mEvent,
prevMEvent,
isFocus,
isEdit,
setEdit,
cancelEdit,
) {
const isBodyOnly = (prevMEvent !== null
&& prevMEvent.getSender() === mEvent.getSender()
&& prevMEvent.getType() !== 'm.room.member'
&& prevMEvent.getType() !== 'm.room.create'
&& diffMinutes(mEvent.getDate(), prevMEvent.getDate()) <= MAX_MSG_DIFF_MINUTES
);
function renderEvent(roomTimeline, mEvent, prevMEvent, isFocus, isEdit, setEdit, cancelEdit) {
const isBodyOnly =
prevMEvent !== null &&
prevMEvent.getSender() === mEvent.getSender() &&
prevMEvent.getType() !== 'm.room.member' &&
prevMEvent.getType() !== 'm.room.create' &&
diffMinutes(mEvent.getDate(), prevMEvent.getDate()) <= MAX_MSG_DIFF_MINUTES;
const timestamp = mEvent.getTs();
if (mEvent.getType() === 'm.room.member') {
@ -221,7 +205,7 @@ function usePaginate(
readUptoEvtStore,
forceUpdateLimit,
timelineScrollRef,
eventLimitRef,
eventLimitRef
) {
const [info, setInfo] = useState(null);
@ -234,10 +218,12 @@ function usePaginate(
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
}
limit.paginate(backwards, PAG_LIMIT, roomTimeline.timeline.length);
setTimeout(() => setInfo({
backwards,
loaded,
}));
setTimeout(() =>
setInfo({
backwards,
loaded,
})
);
};
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
return () => {
@ -283,17 +269,17 @@ function useHandleScroll(
readUptoEvtStore,
forceUpdateLimit,
timelineScrollRef,
eventLimitRef,
eventLimitRef
) {
const handleScroll = useCallback(() => {
const timelineScroll = timelineScrollRef.current;
const limit = eventLimitRef.current;
requestAnimationFrame(() => {
// emit event to toggle scrollToBottom button visibility
const isAtBottom = (
timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()
&& limit.length >= roomTimeline.timeline.length
);
const isAtBottom =
timelineScroll.bottom < 16 &&
!roomTimeline.canPaginateForward() &&
limit.length >= roomTimeline.timeline.length;
roomTimeline.emit(cons.events.roomTimeline.AT_BOTTOM, isAtBottom);
if (isAtBottom && readUptoEvtStore.getItem()) {
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
@ -363,7 +349,8 @@ function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, event
setEvent(event);
return;
}
const isRelates = (event.getType() === 'm.reaction' || event.getRelation()?.rel_type === 'm.replace');
const isRelates =
event.getType() === 'm.reaction' || event.getRelation()?.rel_type === 'm.replace';
if (isRelates) {
setEvent(event);
return;
@ -409,7 +396,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
readUptoEvtStore,
forceUpdateLimit,
timelineScrollRef,
eventLimitRef,
eventLimitRef
);
const [handleScroll, handleScrollToLive] = useHandleScroll(
roomTimeline,
@ -417,7 +404,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
readUptoEvtStore,
forceUpdateLimit,
timelineScrollRef,
eventLimitRef,
eventLimitRef
);
const newEvent = useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, eventLimitRef);
@ -476,41 +463,46 @@ function RoomViewContent({ eventId, roomTimeline }) {
useEffect(() => {
const timelineScroll = timelineScrollRef.current;
if (!roomTimeline.initialized) return;
if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward() && document.visibilityState === 'visible') {
if (
timelineScroll.bottom < 16 &&
!roomTimeline.canPaginateForward() &&
document.visibilityState === 'visible'
) {
timelineScroll.scrollToBottom();
} else {
timelineScroll.tryRestoringScroll();
}
}, [newEvent]);
const listenKeyboard = useCallback((event) => {
if (event.ctrlKey || event.altKey || event.metaKey) return;
if (event.key !== 'ArrowUp') return;
if (navigation.isRawModalVisible) return;
const listenKeyboard = useCallback(
(event) => {
if (event.ctrlKey || event.altKey || event.metaKey) return;
if (event.key !== 'ArrowUp') return;
if (navigation.isRawModalVisible) return;
if (document.activeElement.id !== 'message-textarea') return;
if (document.activeElement.value !== '') return;
if (document.activeElement.id !== 'message-textarea') return;
if (document.activeElement.value !== '') return;
const {
timeline: tl, activeTimeline, liveTimeline, matrixClient: mx,
} = roomTimeline;
const limit = eventLimitRef.current;
if (activeTimeline !== liveTimeline) return;
if (tl.length > limit.length) return;
const { timeline: tl, activeTimeline, liveTimeline, matrixClient: mx } = roomTimeline;
const limit = eventLimitRef.current;
if (activeTimeline !== liveTimeline) return;
if (tl.length > limit.length) return;
const mTypes = ['m.text'];
for (let i = tl.length - 1; i >= 0; i -= 1) {
const mE = tl[i];
if (
mE.getSender() === mx.getUserId()
&& mE.getType() === 'm.room.message'
&& mTypes.includes(mE.getContent()?.msgtype)
) {
setEditEventId(mE.getId());
return;
const mTypes = ['m.text'];
for (let i = tl.length - 1; i >= 0; i -= 1) {
const mE = tl[i];
if (
mE.getSender() === mx.getUserId() &&
mE.getType() === 'm.room.message' &&
mTypes.includes(mE.getContent()?.msgtype)
) {
setEditEventId(mE.getId());
return;
}
}
}
}, [roomTimeline]);
},
[roomTimeline]
);
useEffect(() => {
document.body.addEventListener('keydown', listenKeyboard);
@ -551,7 +543,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
if (i === 0 && !roomTimeline.canPaginateBackward()) {
if (mEvent.getType() === 'm.room.create') {
tl.push(
<RoomIntroContainer key={mEvent.getId()} event={mEvent} timeline={roomTimeline} />,
<RoomIntroContainer key={mEvent.getId()} event={mEvent} timeline={roomTimeline} />
);
itemCountIndex += 1;
// eslint-disable-next-line no-continue
@ -564,9 +556,10 @@ function RoomViewContent({ eventId, roomTimeline }) {
let isNewEvent = false;
if (!unreadDivider) {
unreadDivider = (readUptoEvent
&& prevMEvent?.getTs() <= readUptoEvent.getTs()
&& readUptoEvent.getTs() < mEvent.getTs());
unreadDivider =
readUptoEvent &&
prevMEvent?.getTs() <= readUptoEvent.getTs() &&
readUptoEvent.getTs() < mEvent.getTs();
if (unreadDivider) {
isNewEvent = true;
tl.push(<Divider key={`new-${mEvent.getId()}`} variant="positive" text="New messages" />);
@ -576,7 +569,12 @@ function RoomViewContent({ eventId, roomTimeline }) {
}
const dayDivider = prevMEvent && !isInSameDay(mEvent.getDate(), prevMEvent.getDate());
if (dayDivider) {
tl.push(<Divider key={`divider-${mEvent.getId()}`} text={`${dateFormat(mEvent.getDate(), 'mmmm dd, yyyy')}`} />);
tl.push(
<Divider
key={`divider-${mEvent.getId()}`}
text={`${dateFormat(mEvent.getDate(), 'mmmm dd, yyyy')}`}
/>
);
itemCountIndex += 1;
}
@ -584,15 +582,17 @@ function RoomViewContent({ eventId, roomTimeline }) {
const isFocus = focusId === mEvent.getId();
if (isFocus) jumpToItemIndex = itemCountIndex;
tl.push(renderEvent(
roomTimeline,
mEvent,
isNewEvent ? null : prevMEvent,
isFocus,
editEventId === mEvent.getId(),
setEditEventId,
cancelEdit,
));
tl.push(
renderEvent(
roomTimeline,
mEvent,
isNewEvent ? null : prevMEvent,
isFocus,
editEventId === mEvent.getId(),
setEditEventId,
cancelEdit
)
);
itemCountIndex += 1;
}
if (roomTimeline.canPaginateForward() || limit.length < timeline.length) {
@ -606,7 +606,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
<ScrollView onScroll={handleTimelineScroll} ref={timelineSVRef} autoHide>
<div className="room-view__content" onClick={handleOnClickCapture}>
<div className="timeline__wrapper">
{ roomTimeline.initialized ? renderTimeline() : loadingMsgPlaceholders('loading', 3) }
{roomTimeline.initialized ? renderTimeline() : loadingMsgPlaceholders('loading', 3)}
</div>
</div>
</ScrollView>

View file

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

View file

@ -1,7 +1,5 @@
import React from 'react';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil';
@ -10,83 +8,83 @@ function getTimelineJSXMessages() {
join(user) {
return (
<>
<b>{twemojify(user)}</b>
<b>{user}</b>
{' joined the room'}
</>
);
},
leave(user, reason) {
const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
const reasonMsg = typeof reason === 'string' ? `: ${reason}` : '';
return (
<>
<b>{twemojify(user)}</b>
<b>{user}</b>
{' left the room'}
{twemojify(reasonMsg)}
{reasonMsg}
</>
);
},
invite(inviter, user) {
return (
<>
<b>{twemojify(inviter)}</b>
<b>{inviter}</b>
{' invited '}
<b>{twemojify(user)}</b>
<b>{user}</b>
</>
);
},
cancelInvite(inviter, user) {
return (
<>
<b>{twemojify(inviter)}</b>
<b>{inviter}</b>
{' canceled '}
<b>{twemojify(user)}</b>
{'\'s invite'}
<b>{user}</b>
{"'s invite"}
</>
);
},
rejectInvite(user) {
return (
<>
<b>{twemojify(user)}</b>
<b>{user}</b>
{' rejected the invitation'}
</>
);
},
kick(actor, user, reason) {
const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
const reasonMsg = typeof reason === 'string' ? `: ${reason}` : '';
return (
<>
<b>{twemojify(actor)}</b>
<b>{actor}</b>
{' kicked '}
<b>{twemojify(user)}</b>
{twemojify(reasonMsg)}
<b>{user}</b>
{reasonMsg}
</>
);
},
ban(actor, user, reason) {
const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
const reasonMsg = typeof reason === 'string' ? `: ${reason}` : '';
return (
<>
<b>{twemojify(actor)}</b>
<b>{actor}</b>
{' banned '}
<b>{twemojify(user)}</b>
{twemojify(reasonMsg)}
<b>{user}</b>
{reasonMsg}
</>
);
},
unban(actor, user) {
return (
<>
<b>{twemojify(actor)}</b>
<b>{actor}</b>
{' unbanned '}
<b>{twemojify(user)}</b>
<b>{user}</b>
</>
);
},
avatarSets(user) {
return (
<>
<b>{twemojify(user)}</b>
<b>{user}</b>
{' set a avatar'}
</>
);
@ -94,7 +92,7 @@ function getTimelineJSXMessages() {
avatarChanged(user) {
return (
<>
<b>{twemojify(user)}</b>
<b>{user}</b>
{' changed their avatar'}
</>
);
@ -102,7 +100,7 @@ function getTimelineJSXMessages() {
avatarRemoved(user) {
return (
<>
<b>{twemojify(user)}</b>
<b>{user}</b>
{' removed their avatar'}
</>
);
@ -110,27 +108,27 @@ function getTimelineJSXMessages() {
nameSets(user, newName) {
return (
<>
<b>{twemojify(user)}</b>
<b>{user}</b>
{' set display name to '}
<b>{twemojify(newName)}</b>
<b>{newName}</b>
</>
);
},
nameChanged(user, newName) {
return (
<>
<b>{twemojify(user)}</b>
<b>{user}</b>
{' changed their display name to '}
<b>{twemojify(newName)}</b>
<b>{newName}</b>
</>
);
},
nameRemoved(user, lastName) {
return (
<>
<b>{twemojify(user)}</b>
<b>{user}</b>
{' removed their display name '}
<b>{twemojify(lastName)}</b>
<b>{lastName}</b>
</>
);
},
@ -143,28 +141,46 @@ function getUsersActionJsx(roomId, userIds, actionStr) {
if (room?.getMember(userId)) return getUsernameOfRoomMember(room.getMember(userId));
return getUsername(userId);
};
const getUserJSX = (userId) => <b>{twemojify(getUserDisplayName(userId))}</b>;
const getUserJSX = (userId) => <b>{getUserDisplayName(userId)}</b>;
if (!Array.isArray(userIds)) return 'Idle';
if (userIds.length === 0) return 'Idle';
const MAX_VISIBLE_COUNT = 3;
const u1Jsx = getUserJSX(userIds[0]);
// eslint-disable-next-line react/jsx-one-expression-per-line
if (userIds.length === 1) return <>{u1Jsx} is {actionStr}</>;
if (userIds.length === 1)
return (
<>
{u1Jsx} is {actionStr}
</>
);
const u2Jsx = getUserJSX(userIds[1]);
// eslint-disable-next-line react/jsx-one-expression-per-line
if (userIds.length === 2) return <>{u1Jsx} and {u2Jsx} are {actionStr}</>;
if (userIds.length === 2)
return (
<>
{u1Jsx} and {u2Jsx} are {actionStr}
</>
);
const u3Jsx = getUserJSX(userIds[2]);
if (userIds.length === 3) {
// eslint-disable-next-line react/jsx-one-expression-per-line
return <>{u1Jsx}, {u2Jsx} and {u3Jsx} are {actionStr}</>;
return (
<>
{u1Jsx}, {u2Jsx} and {u3Jsx} are {actionStr}
</>
);
}
const othersCount = userIds.length - MAX_VISIBLE_COUNT;
// eslint-disable-next-line react/jsx-one-expression-per-line
return <>{u1Jsx}, {u2Jsx}, {u3Jsx} and {othersCount} others are {actionStr}</>;
return (
<>
{u1Jsx}, {u2Jsx}, {u3Jsx} and {othersCount} others are {actionStr}
</>
);
}
function parseTimelineChange(mEvent) {
@ -180,18 +196,27 @@ function parseTimelineChange(mEvent) {
const userName = getUsername(mEvent.getStateKey());
switch (content.membership) {
case 'invite': return makeReturnObj('invite', tJSXMsgs.invite(senderName, userName));
case 'ban': return makeReturnObj('leave', tJSXMsgs.ban(senderName, userName, content.reason));
case 'invite':
return makeReturnObj('invite', tJSXMsgs.invite(senderName, userName));
case 'ban':
return makeReturnObj('leave', tJSXMsgs.ban(senderName, userName, content.reason));
case 'join':
if (prevContent.membership === 'join') {
if (content.displayname !== prevContent.displayname) {
if (typeof content.displayname === 'undefined') return makeReturnObj('avatar', tJSXMsgs.nameRemoved(sender, prevContent.displayname));
if (typeof prevContent.displayname === 'undefined') return makeReturnObj('avatar', tJSXMsgs.nameSets(sender, content.displayname));
return makeReturnObj('avatar', tJSXMsgs.nameChanged(prevContent.displayname, content.displayname));
if (typeof content.displayname === 'undefined')
return makeReturnObj('avatar', tJSXMsgs.nameRemoved(sender, prevContent.displayname));
if (typeof prevContent.displayname === 'undefined')
return makeReturnObj('avatar', tJSXMsgs.nameSets(sender, content.displayname));
return makeReturnObj(
'avatar',
tJSXMsgs.nameChanged(prevContent.displayname, content.displayname)
);
}
if (content.avatar_url !== prevContent.avatar_url) {
if (typeof content.avatar_url === 'undefined') return makeReturnObj('avatar', tJSXMsgs.avatarRemoved(content.displayname));
if (typeof prevContent.avatar_url === 'undefined') return makeReturnObj('avatar', tJSXMsgs.avatarSets(content.displayname));
if (typeof content.avatar_url === 'undefined')
return makeReturnObj('avatar', tJSXMsgs.avatarRemoved(content.displayname));
if (typeof prevContent.avatar_url === 'undefined')
return makeReturnObj('avatar', tJSXMsgs.avatarSets(content.displayname));
return makeReturnObj('avatar', tJSXMsgs.avatarChanged(content.displayname));
}
return null;
@ -200,23 +225,25 @@ function parseTimelineChange(mEvent) {
case 'leave':
if (sender === mEvent.getStateKey()) {
switch (prevContent.membership) {
case 'invite': return makeReturnObj('invite-cancel', tJSXMsgs.rejectInvite(senderName));
default: return makeReturnObj('leave', tJSXMsgs.leave(senderName, content.reason));
case 'invite':
return makeReturnObj('invite-cancel', tJSXMsgs.rejectInvite(senderName));
default:
return makeReturnObj('leave', tJSXMsgs.leave(senderName, content.reason));
}
}
switch (prevContent.membership) {
case 'invite': return makeReturnObj('invite-cancel', tJSXMsgs.cancelInvite(senderName, userName));
case 'ban': return makeReturnObj('other', tJSXMsgs.unban(senderName, userName));
case 'invite':
return makeReturnObj('invite-cancel', tJSXMsgs.cancelInvite(senderName, userName));
case 'ban':
return makeReturnObj('other', tJSXMsgs.unban(senderName, userName));
// sender is not target and made the target leave,
// if not from invite/ban then this is a kick
default: return makeReturnObj('leave', tJSXMsgs.kick(senderName, userName, content.reason));
default:
return makeReturnObj('leave', tJSXMsgs.kick(senderName, userName, content.reason));
}
default: return null;
default:
return null;
}
}
export {
getTimelineJSXMessages,
getUsersActionJsx,
parseTimelineChange,
};
export { getTimelineJSXMessages, getUsersActionJsx, parseTimelineChange };

View file

@ -3,7 +3,6 @@ import React, { useState } from 'react';
import './CrossSigning.scss';
import FileSaver from 'file-saver';
import { Formik } from 'formik';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import { openReusableDialog } from '../../../client/action/navigation';
@ -22,15 +21,17 @@ import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
const failedDialog = () => {
const renderFailure = (requestClose) => (
<div className="cross-signing__failure">
<Text variant="h1">{twemojify('❌')}</Text>
<Text variant="h1">{'❌'}</Text>
<Text weight="medium">Failed to setup cross signing. Please try again.</Text>
<Button onClick={requestClose}>Close</Button>
</div>
);
openReusableDialog(
<Text variant="s1" weight="medium">Setup cross signing</Text>,
renderFailure,
<Text variant="s1" weight="medium">
Setup cross signing
</Text>,
renderFailure
);
};
@ -48,11 +49,11 @@ const securityKeyDialog = (key) => {
const renderSecurityKey = () => (
<div className="cross-signing__key">
<Text weight="medium">Please save this security key somewhere safe.</Text>
<Text className="cross-signing__key-text">
{key.encodedPrivateKey}
</Text>
<Text className="cross-signing__key-text">{key.encodedPrivateKey}</Text>
<div className="cross-signing__key-btn">
<Button variant="primary" onClick={() => copyKey(key)}>Copy</Button>
<Button variant="primary" onClick={() => copyKey(key)}>
Copy
</Button>
<Button onClick={() => downloadKey(key)}>Download</Button>
</div>
</div>
@ -62,8 +63,10 @@ const securityKeyDialog = (key) => {
downloadKey();
openReusableDialog(
<Text variant="s1" weight="medium">Security Key</Text>,
() => renderSecurityKey(),
<Text variant="s1" weight="medium">
Security Key
</Text>,
() => renderSecurityKey()
);
};
@ -112,7 +115,7 @@ function CrossSigningSetup() {
errors.phrase = 'Phrase must contain 8-127 characters with no space.';
}
if (values.confirmPhrase.length > 0 && values.confirmPhrase !== values.phrase) {
errors.confirmPhrase = 'Phrase don\'t match.';
errors.confirmPhrase = "Phrase don't match.";
}
return errors;
};
@ -121,10 +124,14 @@ function CrossSigningSetup() {
<div className="cross-signing__setup">
<div className="cross-signing__setup-entry">
<Text>
We will generate a <b>Security Key</b>,
which you can use to manage messages backup and session verification.
We will generate a <b>Security Key</b>, which you can use to manage messages backup and
session verification.
</Text>
{genWithPhrase !== false && <Button variant="primary" onClick={() => setup()} disabled={genWithPhrase !== undefined}>Generate Key</Button>}
{genWithPhrase !== false && (
<Button variant="primary" onClick={() => setup()} disabled={genWithPhrase !== undefined}>
Generate Key
</Button>
)}
{genWithPhrase === false && <Spinner size="small" />}
</div>
<Text className="cross-signing__setup-divider">OR</Text>
@ -133,9 +140,7 @@ function CrossSigningSetup() {
onSubmit={(values) => setup(values.phrase)}
validate={validator}
>
{({
values, errors, handleChange, handleSubmit,
}) => (
{({ values, errors, handleChange, handleSubmit }) => (
<form
className="cross-signing__setup-entry"
onSubmit={handleSubmit}
@ -143,8 +148,8 @@ function CrossSigningSetup() {
>
<Text>
Alternatively you can also set a <b>Security Phrase </b>
so you don't have to remember long Security Key,
and optionally save the Key as backup.
so you don't have to remember long Security Key, and optionally save the Key as
backup.
</Text>
<Input
name="phrase"
@ -155,7 +160,11 @@ function CrossSigningSetup() {
required
disabled={genWithPhrase !== undefined}
/>
{errors.phrase && <Text variant="b3" className="cross-signing__error">{errors.phrase}</Text>}
{errors.phrase && (
<Text variant="b3" className="cross-signing__error">
{errors.phrase}
</Text>
)}
<Input
name="confirmPhrase"
value={values.confirmPhrase}
@ -165,8 +174,16 @@ function CrossSigningSetup() {
required
disabled={genWithPhrase !== undefined}
/>
{errors.confirmPhrase && <Text variant="b3" className="cross-signing__error">{errors.confirmPhrase}</Text>}
{genWithPhrase !== true && <Button variant="primary" type="submit" disabled={genWithPhrase !== undefined}>Set Phrase & Generate Key</Button>}
{errors.confirmPhrase && (
<Text variant="b3" className="cross-signing__error">
{errors.confirmPhrase}
</Text>
)}
{genWithPhrase !== true && (
<Button variant="primary" type="submit" disabled={genWithPhrase !== undefined}>
Set Phrase & Generate Key
</Button>
)}
{genWithPhrase === true && <Spinner size="small" />}
</form>
)}
@ -177,31 +194,36 @@ function CrossSigningSetup() {
const setupDialog = () => {
openReusableDialog(
<Text variant="s1" weight="medium">Setup cross signing</Text>,
() => <CrossSigningSetup />,
<Text variant="s1" weight="medium">
Setup cross signing
</Text>,
() => <CrossSigningSetup />
);
};
function CrossSigningReset() {
return (
<div className="cross-signing__reset">
<Text variant="h1">{twemojify('✋🧑‍🚒🤚')}</Text>
<Text variant="h1">{'✋🧑‍🚒🤚'}</Text>
<Text weight="medium">Resetting cross-signing keys is permanent.</Text>
<Text>
Anyone you have verified with will see security alerts and your message backup will be lost.
You almost certainly do not want to do this,
unless you have lost <b>Security Key</b> or <b>Phrase</b> and
every session you can cross-sign from.
Anyone you have verified with will see security alerts and your message backup will be lost.
You almost certainly do not want to do this, unless you have lost <b>Security Key</b> or{' '}
<b>Phrase</b> and every session you can cross-sign from.
</Text>
<Button variant="danger" onClick={setupDialog}>Reset</Button>
<Button variant="danger" onClick={setupDialog}>
Reset
</Button>
</div>
);
}
const resetDialog = () => {
openReusableDialog(
<Text variant="s1" weight="medium">Reset cross signing</Text>,
() => <CrossSigningReset />,
<Text variant="s1" weight="medium">
Reset cross signing
</Text>,
() => <CrossSigningReset />
);
};
@ -210,12 +232,23 @@ function CrossSignin() {
return (
<SettingTile
title="Cross signing"
content={<Text variant="b3">Setup to verify and keep track of all your sessions. Also required to backup encrypted message.</Text>}
options={(
isCSEnabled
? <Button variant="danger" onClick={resetDialog}>Reset</Button>
: <Button variant="primary" onClick={setupDialog}>Setup</Button>
)}
content={
<Text variant="b3">
Setup to verify and keep track of all your sessions. Also required to backup encrypted
message.
</Text>
}
options={
isCSEnabled ? (
<Button variant="danger" onClick={resetDialog}>
Reset
</Button>
) : (
<Button variant="primary" onClick={setupDialog}>
Setup
</Button>
)
}
/>
);
}

View file

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

View file

@ -80,7 +80,7 @@ function AppearanceSection() {
{ text: 'Butter' },
{ text: 'Nord Dark' },
{ text: 'Cyberpunk' },
{ text: 'Almond Dark'},
{ text: 'Almond Dark' },
]}
onSelect={(index) => {
if (settings.useSystemTheme) toggleSystemTheme();
@ -328,28 +328,6 @@ function AboutSection() {
.
</Text>
</li>
<li>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */}
<Text>
The{' '}
<a href="https://twemoji.twitter.com" target="_blank" rel="noreferrer noopener">
Twemoji
</a>{' '}
emoji art is ©{' '}
<a href="https://twemoji.twitter.com" target="_blank" rel="noreferrer noopener">
Twitter, Inc and other contributors
</a>{' '}
used under the terms of{' '}
<a
href="https://creativecommons.org/licenses/by/4.0/"
target="_blank"
rel="noreferrer noopener"
>
CC-BY 4.0
</a>
.
</Text>
</li>
<li>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */}
<Text>

View file

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

View file

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