mirror of
https://github.com/array-in-a-matrix/xinny.git
synced 2024-05-08 16:03:34 -04:00
Compare commits
6 commits
0cc57aae49
...
0aacd4f75b
Author | SHA1 | Date | |
---|---|---|---|
0aacd4f75b | |||
b1aceec240 | |||
6da274d1a9 | |||
5c5acbd763 | |||
d6c8036a81 | |||
57fc0414ab |
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 can’t disable this later. Bridges & most bots won’t work yet.</Text>}
|
||||
content={
|
||||
<Text variant="b3">
|
||||
You can’t disable this later. Bridges & most bots won’t 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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
));
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
Loading…
Reference in a new issue