mirror of
https://github.com/array-in-a-matrix/xinny.git
synced 2024-05-19 21:00:17 -04:00
Compare commits
5 commits
cc116ad8dc
...
b1aceec240
Author | SHA1 | Date | |
---|---|---|---|
b1aceec240 | |||
6da274d1a9 | |||
5c5acbd763 | |||
d6c8036a81 | |||
57fc0414ab |
|
@ -2,17 +2,13 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './Avatar.scss';
|
import './Avatar.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import Text from '../text/Text';
|
import Text from '../text/Text';
|
||||||
import RawIcon from '../system-icons/RawIcon';
|
import RawIcon from '../system-icons/RawIcon';
|
||||||
|
|
||||||
import ImageBrokenSVG from '../../../../public/res/svg/image-broken.svg';
|
import ImageBrokenSVG from '../../../../public/res/svg/image-broken.svg';
|
||||||
import { avatarInitials } from '../../../util/common';
|
import { avatarInitials } from '../../../util/common';
|
||||||
|
|
||||||
const Avatar = React.forwardRef(({
|
const Avatar = React.forwardRef(({ text, bgColor, iconSrc, iconColor, imageSrc, size }, ref) => {
|
||||||
text, bgColor, iconSrc, iconColor, imageSrc, size,
|
|
||||||
}, ref) => {
|
|
||||||
let textSize = 's1';
|
let textSize = 's1';
|
||||||
if (size === 'large') textSize = 'h1';
|
if (size === 'large') textSize = 'h1';
|
||||||
if (size === 'small') textSize = 'b1';
|
if (size === 'small') textSize = 'b1';
|
||||||
|
@ -20,34 +16,34 @@ const Avatar = React.forwardRef(({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={`avatar-container avatar-container__${size} noselect`}>
|
<div ref={ref} className={`avatar-container avatar-container__${size} noselect`}>
|
||||||
{
|
{imageSrc !== null ? (
|
||||||
imageSrc !== null
|
<img
|
||||||
? (
|
draggable="false"
|
||||||
<img
|
src={imageSrc}
|
||||||
draggable="false"
|
onLoad={(e) => {
|
||||||
src={imageSrc}
|
e.target.style.backgroundColor = 'transparent';
|
||||||
onLoad={(e) => { e.target.style.backgroundColor = 'transparent'; }}
|
}}
|
||||||
onError={(e) => { e.target.src = ImageBrokenSVG; }}
|
onError={(e) => {
|
||||||
alt=""
|
e.target.src = ImageBrokenSVG;
|
||||||
/>
|
}}
|
||||||
)
|
alt=""
|
||||||
: (
|
/>
|
||||||
<span
|
) : (
|
||||||
style={{ backgroundColor: iconSrc === null ? bgColor : 'transparent' }}
|
<span
|
||||||
className={`avatar__border${iconSrc !== null ? '--active' : ''}`}
|
style={{ backgroundColor: iconSrc === null ? bgColor : 'transparent' }}
|
||||||
>
|
className={`avatar__border${iconSrc !== null ? '--active' : ''}`}
|
||||||
{
|
>
|
||||||
iconSrc !== null
|
{iconSrc !== null ? (
|
||||||
? <RawIcon size={size} src={iconSrc} color={iconColor} />
|
<RawIcon size={size} src={iconSrc} color={iconColor} />
|
||||||
: text !== null && (
|
) : (
|
||||||
<Text variant={textSize} primary>
|
text !== null && (
|
||||||
{twemojify(avatarInitials(text))}
|
<Text variant={textSize} primary>
|
||||||
</Text>
|
{avatarInitials(text)}
|
||||||
)
|
</Text>
|
||||||
}
|
)
|
||||||
</span>
|
)}
|
||||||
)
|
</span>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,16 +2,21 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './Dialog.scss';
|
import './Dialog.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import Header, { TitleWrapper } from '../../atoms/header/Header';
|
import Header, { TitleWrapper } from '../../atoms/header/Header';
|
||||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||||
import RawModal from '../../atoms/modal/RawModal';
|
import RawModal from '../../atoms/modal/RawModal';
|
||||||
|
|
||||||
function Dialog({
|
function Dialog({
|
||||||
className, isOpen, title, onAfterOpen, onAfterClose,
|
className,
|
||||||
contentOptions, onRequestClose, closeFromOutside, children,
|
isOpen,
|
||||||
|
title,
|
||||||
|
onAfterOpen,
|
||||||
|
onAfterClose,
|
||||||
|
contentOptions,
|
||||||
|
onRequestClose,
|
||||||
|
closeFromOutside,
|
||||||
|
children,
|
||||||
invisibleScroll,
|
invisibleScroll,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
@ -28,19 +33,19 @@ function Dialog({
|
||||||
<div className="dialog__content">
|
<div className="dialog__content">
|
||||||
<Header>
|
<Header>
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
{
|
{typeof title === 'string' ? (
|
||||||
typeof title === 'string'
|
<Text variant="h2" weight="medium" primary>
|
||||||
? <Text variant="h2" weight="medium" primary>{twemojify(title)}</Text>
|
{title}
|
||||||
: title
|
</Text>
|
||||||
}
|
) : (
|
||||||
|
title
|
||||||
|
)}
|
||||||
</TitleWrapper>
|
</TitleWrapper>
|
||||||
{contentOptions}
|
{contentOptions}
|
||||||
</Header>
|
</Header>
|
||||||
<div className="dialog__content__wrapper">
|
<div className="dialog__content__wrapper">
|
||||||
<ScrollView autoHide={!invisibleScroll} invisible={invisibleScroll}>
|
<ScrollView autoHide={!invisibleScroll} invisible={invisibleScroll}>
|
||||||
<div className="dialog__content-container">
|
<div className="dialog__content-container">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
/* eslint-disable react/prop-types */
|
/* eslint-disable react/prop-types */
|
||||||
import React, {
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
useState, useEffect, useCallback, useRef,
|
|
||||||
} from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './Message.scss';
|
import './Message.scss';
|
||||||
|
|
||||||
|
@ -9,13 +7,20 @@ import { twemojify } from '../../../util/twemojify';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import {
|
import {
|
||||||
getUsername, getUsernameOfRoomMember, parseReply, trimHTMLReply,
|
getUsername,
|
||||||
|
getUsernameOfRoomMember,
|
||||||
|
parseReply,
|
||||||
|
trimHTMLReply,
|
||||||
} from '../../../util/matrixUtil';
|
} from '../../../util/matrixUtil';
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
import { getEventCords } from '../../../util/common';
|
import { getEventCords } from '../../../util/common';
|
||||||
import { redactEvent, sendReaction } from '../../../client/action/roomTimeline';
|
import { redactEvent, sendReaction } from '../../../client/action/roomTimeline';
|
||||||
import {
|
import {
|
||||||
openEmojiBoard, openProfileViewer, openReadReceipts, openViewSource, replyTo,
|
openEmojiBoard,
|
||||||
|
openProfileViewer,
|
||||||
|
openReadReceipts,
|
||||||
|
openViewSource,
|
||||||
|
replyTo,
|
||||||
} from '../../../client/action/navigation';
|
} from '../../../client/action/navigation';
|
||||||
import { sanitizeCustomHtml } from '../../../util/sanitize';
|
import { sanitizeCustomHtml } from '../../../util/sanitize';
|
||||||
|
|
||||||
|
@ -27,7 +32,11 @@ import Input from '../../atoms/input/Input';
|
||||||
import Avatar from '../../atoms/avatar/Avatar';
|
import Avatar from '../../atoms/avatar/Avatar';
|
||||||
import IconButton from '../../atoms/button/IconButton';
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
import Time from '../../atoms/time/Time';
|
import Time from '../../atoms/time/Time';
|
||||||
import ContextMenu, { MenuHeader, MenuItem, MenuBorder } from '../../atoms/context-menu/ContextMenu';
|
import ContextMenu, {
|
||||||
|
MenuHeader,
|
||||||
|
MenuItem,
|
||||||
|
MenuBorder,
|
||||||
|
} from '../../atoms/context-menu/ContextMenu';
|
||||||
import * as Media from '../media/Media';
|
import * as Media from '../media/Media';
|
||||||
|
|
||||||
import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg';
|
import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg';
|
||||||
|
@ -61,9 +70,7 @@ function PlaceholderMessage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageAvatar = React.memo(({
|
const MessageAvatar = React.memo(({ roomId, avatarSrc, userId, username }) => (
|
||||||
roomId, avatarSrc, userId, username,
|
|
||||||
}) => (
|
|
||||||
<div className="message__avatar-container">
|
<div className="message__avatar-container">
|
||||||
<button type="button" onClick={() => openProfileViewer(userId, roomId)}>
|
<button type="button" onClick={() => openProfileViewer(userId, roomId)}>
|
||||||
<Avatar imageSrc={avatarSrc} text={username} bgColor={colorMXID(userId)} size="small" />
|
<Avatar imageSrc={avatarSrc} text={username} bgColor={colorMXID(userId)} size="small" />
|
||||||
|
@ -71,9 +78,7 @@ const MessageAvatar = React.memo(({
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
|
|
||||||
const MessageHeader = React.memo(({
|
const MessageHeader = React.memo(({ userId, username, timestamp, fullTime }) => (
|
||||||
userId, username, timestamp, fullTime,
|
|
||||||
}) => (
|
|
||||||
<div className="message__header">
|
<div className="message__header">
|
||||||
<Text
|
<Text
|
||||||
style={{ color: colorMXID(userId) }}
|
style={{ color: colorMXID(userId) }}
|
||||||
|
@ -82,8 +87,8 @@ const MessageHeader = React.memo(({
|
||||||
weight="medium"
|
weight="medium"
|
||||||
span
|
span
|
||||||
>
|
>
|
||||||
<span>{twemojify(username)}</span>
|
<span>{username}</span>
|
||||||
<span>{twemojify(userId)}</span>
|
<span>{userId}</span>
|
||||||
</Text>
|
</Text>
|
||||||
<div className="message__time">
|
<div className="message__time">
|
||||||
<Text variant="b3">
|
<Text variant="b3">
|
||||||
|
@ -107,9 +112,7 @@ function MessageReply({ name, color, body }) {
|
||||||
<div className="message__reply">
|
<div className="message__reply">
|
||||||
<Text variant="b2">
|
<Text variant="b2">
|
||||||
<RawIcon color={color} size="extra-small" src={ReplyArrowIC} />
|
<RawIcon color={color} size="extra-small" src={ReplyArrowIC} />
|
||||||
<span style={{ color }}>{twemojify(name)}</span>
|
<span style={{ color }}>{name}</span> {body}
|
||||||
{' '}
|
|
||||||
{twemojify(body)}
|
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -143,7 +146,9 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
|
||||||
const username = getUsernameOfRoomMember(mEvent.sender);
|
const username = getUsernameOfRoomMember(mEvent.sender);
|
||||||
|
|
||||||
if (isMountedRef.current === false) return;
|
if (isMountedRef.current === false) return;
|
||||||
const fallbackBody = mEvent.isRedacted() ? '*** This message has been deleted ***' : '*** Unable to load reply ***';
|
const fallbackBody = mEvent.isRedacted()
|
||||||
|
? '*** This message has been deleted ***'
|
||||||
|
: '*** Unable to load reply ***';
|
||||||
let parsedBody = parseReply(rawBody)?.body ?? rawBody ?? fallbackBody;
|
let parsedBody = parseReply(rawBody)?.body ?? rawBody ?? fallbackBody;
|
||||||
if (editedList && parsedBody.startsWith(' * ')) {
|
if (editedList && parsedBody.startsWith(' * ')) {
|
||||||
parsedBody = parsedBody.slice(3);
|
parsedBody = parsedBody.slice(3);
|
||||||
|
@ -197,13 +202,7 @@ MessageReplyWrapper.propTypes = {
|
||||||
eventId: PropTypes.string.isRequired,
|
eventId: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MessageBody = React.memo(({
|
const MessageBody = React.memo(({ senderName, body, isCustomHTML, isEdited, msgType }) => {
|
||||||
senderName,
|
|
||||||
body,
|
|
||||||
isCustomHTML,
|
|
||||||
isEdited,
|
|
||||||
msgType,
|
|
||||||
}) => {
|
|
||||||
// if body is not string it is a React element.
|
// if body is not string it is a React element.
|
||||||
if (typeof body !== 'string') return <div className="message__body">{body}</div>;
|
if (typeof body !== 'string') return <div className="message__body">{body}</div>;
|
||||||
|
|
||||||
|
@ -215,14 +214,14 @@ const MessageBody = React.memo(({
|
||||||
undefined,
|
undefined,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
true,
|
true
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
console.error('Malformed custom html: ', body);
|
console.error('Malformed custom html: ', body);
|
||||||
content = twemojify(body, undefined);
|
content = body;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
content = twemojify(body, undefined, true);
|
content = body;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if this message should render with large emojis
|
// Determine if this message should render with large emojis
|
||||||
|
@ -240,10 +239,14 @@ const MessageBody = React.memo(({
|
||||||
const nEmojis = content.filter((e) => e.type === 'img').length;
|
const nEmojis = content.filter((e) => e.type === 'img').length;
|
||||||
|
|
||||||
// Make sure there's no text besides whitespace and variation selector U+FE0F
|
// Make sure there's no text besides whitespace and variation selector U+FE0F
|
||||||
if (nEmojis <= 10 && content.every((element) => (
|
if (
|
||||||
(typeof element === 'object' && element.type === 'img')
|
nEmojis <= 10 &&
|
||||||
|| (typeof element === 'string' && /^[\s\ufe0f]*$/g.test(element))
|
content.every(
|
||||||
))) {
|
(element) =>
|
||||||
|
(typeof element === 'object' && element.type === 'img') ||
|
||||||
|
(typeof element === 'string' && /^[\s\ufe0f]*$/g.test(element))
|
||||||
|
)
|
||||||
|
) {
|
||||||
emojiOnly = true;
|
emojiOnly = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -251,22 +254,25 @@ const MessageBody = React.memo(({
|
||||||
if (!isCustomHTML) {
|
if (!isCustomHTML) {
|
||||||
// If this is a plaintext message, wrap it in a <p> element (automatically applying
|
// If this is a plaintext message, wrap it in a <p> element (automatically applying
|
||||||
// white-space: pre-wrap) in order to preserve newlines
|
// white-space: pre-wrap) in order to preserve newlines
|
||||||
content = (<p className="message__body-plain">{content}</p>);
|
content = <p className="message__body-plain">{content}</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="message__body">
|
<div className="message__body">
|
||||||
<div dir="auto" className={`text ${emojiOnly ? 'text-h1' : 'text-b1'}`}>
|
<div dir="auto" className={`text ${emojiOnly ? 'text-h1' : 'text-b1'}`}>
|
||||||
{ msgType === 'm.emote' && (
|
{msgType === 'm.emote' && (
|
||||||
<>
|
<>
|
||||||
{'* '}
|
{'* '}
|
||||||
{twemojify(senderName)}
|
{senderName}{' '}
|
||||||
{' '}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{ content }
|
{content}
|
||||||
</div>
|
</div>
|
||||||
{ isEdited && <Text className="message__body-edited" variant="b3">(edited)</Text>}
|
{isEdited && (
|
||||||
|
<Text className="message__body-edited" variant="b3">
|
||||||
|
(edited)
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -305,7 +311,13 @@ function MessageEdit({ body, onSave, onCancel }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="message__edit" onSubmit={(e) => { e.preventDefault(); onSave(editInputRef.current.value, body); }}>
|
<form
|
||||||
|
className="message__edit"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onSave(editInputRef.current.value, body);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
forwardRef={editInputRef}
|
forwardRef={editInputRef}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
|
@ -316,7 +328,9 @@ function MessageEdit({ body, onSave, onCancel }) {
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<div className="message__edit-btns">
|
<div className="message__edit-btns">
|
||||||
<Button type="submit" variant="primary">Save</Button>
|
<Button type="submit" variant="primary">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
<Button onClick={onCancel}>Cancel</Button>
|
<Button onClick={onCancel}>Cancel</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -366,23 +380,19 @@ function genReactionMsg(userIds, reaction, shortcode) {
|
||||||
<>
|
<>
|
||||||
{userIds.map((userId, index) => (
|
{userIds.map((userId, index) => (
|
||||||
<React.Fragment key={userId}>
|
<React.Fragment key={userId}>
|
||||||
{twemojify(getUsername(userId))}
|
{getUsername(userId)}
|
||||||
{index < userIds.length - 1 && (
|
{index < userIds.length - 1 && (
|
||||||
<span style={{ opacity: '.6' }}>
|
<span style={{ opacity: '.6' }}>{index === userIds.length - 2 ? ' and ' : ', '}</span>
|
||||||
{index === userIds.length - 2 ? ' and ' : ', '}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
<span style={{ opacity: '.6' }}>{' reacted with '}</span>
|
<span style={{ opacity: '.6' }}>{' reacted with '}</span>
|
||||||
{twemojify(shortcode ? `:${shortcode}:` : reaction, { className: 'react-emoji' })}
|
{shortcode ? `:${shortcode}:` : reaction}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MessageReaction({
|
function MessageReaction({ reaction, shortcode, count, users, isActive, onClick }) {
|
||||||
reaction, shortcode, count, users, isActive, onClick,
|
|
||||||
}) {
|
|
||||||
let customEmojiUrl = null;
|
let customEmojiUrl = null;
|
||||||
if (reaction.match(/^mxc:\/\/\S+$/)) {
|
if (reaction.match(/^mxc:\/\/\S+$/)) {
|
||||||
customEmojiUrl = initMatrix.matrixClient.mxcUrlToHttp(reaction);
|
customEmojiUrl = initMatrix.matrixClient.mxcUrlToHttp(reaction);
|
||||||
|
@ -390,19 +400,32 @@ function MessageReaction({
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
className="msg__reaction-tooltip"
|
className="msg__reaction-tooltip"
|
||||||
content={<Text variant="b2">{users.length > 0 ? genReactionMsg(users, reaction, shortcode) : 'Unable to load who has reacted'}</Text>}
|
content={
|
||||||
|
<Text variant="b2">
|
||||||
|
{users.length > 0
|
||||||
|
? genReactionMsg(users, reaction, shortcode)
|
||||||
|
: 'Unable to load who has reacted'}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
type="button"
|
type="button"
|
||||||
className={`msg__reaction${isActive ? ' msg__reaction--active' : ''}`}
|
className={`msg__reaction${isActive ? ' msg__reaction--active' : ''}`}
|
||||||
>
|
>
|
||||||
{
|
{customEmojiUrl ? (
|
||||||
customEmojiUrl
|
<img
|
||||||
? <img className="react-emoji" draggable="false" alt={shortcode ?? reaction} src={customEmojiUrl} />
|
className="react-emoji"
|
||||||
: twemojify(reaction, { className: 'react-emoji' })
|
draggable="false"
|
||||||
}
|
alt={shortcode ?? reaction}
|
||||||
<Text variant="b3" className="msg__reaction-count">{count}</Text>
|
src={customEmojiUrl}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
reaction
|
||||||
|
)}
|
||||||
|
<Text variant="b3" className="msg__reaction-count">
|
||||||
|
{count}
|
||||||
|
</Text>
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
@ -468,21 +491,19 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="message__reactions text text-b3 noselect">
|
<div className="message__reactions text text-b3 noselect">
|
||||||
{
|
{Object.keys(reactions).map((key) => (
|
||||||
Object.keys(reactions).map((key) => (
|
<MessageReaction
|
||||||
<MessageReaction
|
key={key}
|
||||||
key={key}
|
reaction={key}
|
||||||
reaction={key}
|
shortcode={reactions[key].shortcode}
|
||||||
shortcode={reactions[key].shortcode}
|
count={reactions[key].count}
|
||||||
count={reactions[key].count}
|
users={reactions[key].users}
|
||||||
users={reactions[key].users}
|
isActive={reactions[key].isActive}
|
||||||
isActive={reactions[key].isActive}
|
onClick={() => {
|
||||||
onClick={() => {
|
toggleEmoji(roomId, mEvent.getId(), key, reactions[key].shortcode, roomTimeline);
|
||||||
toggleEmoji(roomId, mEvent.getId(), key, reactions[key].shortcode, roomTimeline);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
))}
|
||||||
))
|
|
||||||
}
|
|
||||||
{canSendReaction && (
|
{canSendReaction && (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
@ -503,11 +524,11 @@ MessageReactionGroup.propTypes = {
|
||||||
|
|
||||||
function isMedia(mE) {
|
function isMedia(mE) {
|
||||||
return (
|
return (
|
||||||
mE.getContent()?.msgtype === 'm.file'
|
mE.getContent()?.msgtype === 'm.file' ||
|
||||||
|| mE.getContent()?.msgtype === 'm.image'
|
mE.getContent()?.msgtype === 'm.image' ||
|
||||||
|| mE.getContent()?.msgtype === 'm.audio'
|
mE.getContent()?.msgtype === 'm.audio' ||
|
||||||
|| mE.getContent()?.msgtype === 'm.video'
|
mE.getContent()?.msgtype === 'm.video' ||
|
||||||
|| mE.getType() === 'm.sticker'
|
mE.getType() === 'm.sticker'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -523,9 +544,7 @@ function handleOpenViewSource(mEvent, roomTimeline) {
|
||||||
openViewSource(editedMEvent !== undefined ? editedMEvent : mEvent);
|
openViewSource(editedMEvent !== undefined ? editedMEvent : mEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageOptions = React.memo(({
|
const MessageOptions = React.memo(({ roomTimeline, mEvent, edit, reply }) => {
|
||||||
roomTimeline, mEvent, edit, reply,
|
|
||||||
}) => {
|
|
||||||
const { roomId, room } = roomTimeline;
|
const { roomId, room } = roomTimeline;
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const senderId = mEvent.getSender();
|
const senderId = mEvent.getSender();
|
||||||
|
@ -544,19 +563,9 @@ const MessageOptions = React.memo(({
|
||||||
tooltip="Add reaction"
|
tooltip="Add reaction"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<IconButton
|
<IconButton onClick={() => reply()} src={ReplyArrowIC} size="extra-small" tooltip="Reply" />
|
||||||
onClick={() => reply()}
|
{senderId === mx.getUserId() && !isMedia(mEvent) && (
|
||||||
src={ReplyArrowIC}
|
<IconButton onClick={() => edit(true)} src={PencilIC} size="extra-small" tooltip="Edit" />
|
||||||
size="extra-small"
|
|
||||||
tooltip="Reply"
|
|
||||||
/>
|
|
||||||
{(senderId === mx.getUserId() && !isMedia(mEvent)) && (
|
|
||||||
<IconButton
|
|
||||||
onClick={() => edit(true)}
|
|
||||||
src={PencilIC}
|
|
||||||
size="extra-small"
|
|
||||||
tooltip="Edit"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
content={() => (
|
content={() => (
|
||||||
|
@ -568,10 +577,7 @@ const MessageOptions = React.memo(({
|
||||||
>
|
>
|
||||||
Read receipts
|
Read receipts
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem iconSrc={CmdIC} onClick={() => handleOpenViewSource(mEvent, roomTimeline)}>
|
||||||
iconSrc={CmdIC}
|
|
||||||
onClick={() => handleOpenViewSource(mEvent, roomTimeline)}
|
|
||||||
>
|
|
||||||
View source
|
View source
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{(canIRedact || senderId === mx.getUserId()) && (
|
{(canIRedact || senderId === mx.getUserId()) && (
|
||||||
|
@ -585,7 +591,7 @@ const MessageOptions = React.memo(({
|
||||||
'Delete message',
|
'Delete message',
|
||||||
'Are you sure that you want to delete this message?',
|
'Are you sure that you want to delete this message?',
|
||||||
'Delete',
|
'Delete',
|
||||||
'danger',
|
'danger'
|
||||||
);
|
);
|
||||||
if (!isConfirmed) return;
|
if (!isConfirmed) return;
|
||||||
redactEvent(roomId, mEvent.getId());
|
redactEvent(roomId, mEvent.getId());
|
||||||
|
@ -619,7 +625,8 @@ MessageOptions.propTypes = {
|
||||||
function genMediaContent(mE) {
|
function genMediaContent(mE) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const mContent = mE.getContent();
|
const mContent = mE.getContent();
|
||||||
if (!mContent || !mContent.body) return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
if (!mContent || !mContent.body)
|
||||||
|
return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
||||||
|
|
||||||
let mediaMXC = mContent?.url;
|
let mediaMXC = mContent?.url;
|
||||||
const isEncryptedFile = typeof mediaMXC === 'undefined';
|
const isEncryptedFile = typeof mediaMXC === 'undefined';
|
||||||
|
@ -627,7 +634,8 @@ function genMediaContent(mE) {
|
||||||
|
|
||||||
let thumbnailMXC = mContent?.info?.thumbnail_url;
|
let thumbnailMXC = mContent?.info?.thumbnail_url;
|
||||||
|
|
||||||
if (typeof mediaMXC === 'undefined' || mediaMXC === '') return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
if (typeof mediaMXC === 'undefined' || mediaMXC === '')
|
||||||
|
return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
||||||
|
|
||||||
let msgType = mE.getContent()?.msgtype;
|
let msgType = mE.getContent()?.msgtype;
|
||||||
const safeMimetype = getBlobSafeMimeType(mContent.info?.mimetype);
|
const safeMimetype = getBlobSafeMimeType(mContent.info?.mimetype);
|
||||||
|
@ -717,13 +725,19 @@ function getEditedBody(editedMEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Message({
|
function Message({
|
||||||
mEvent, isBodyOnly, roomTimeline,
|
mEvent,
|
||||||
focus, fullTime, isEdit, setEdit, cancelEdit,
|
isBodyOnly,
|
||||||
|
roomTimeline,
|
||||||
|
focus,
|
||||||
|
fullTime,
|
||||||
|
isEdit,
|
||||||
|
setEdit,
|
||||||
|
cancelEdit,
|
||||||
}) {
|
}) {
|
||||||
const roomId = mEvent.getRoomId();
|
const roomId = mEvent.getRoomId();
|
||||||
const { editedTimeline, reactionTimeline } = roomTimeline ?? {};
|
const { editedTimeline, reactionTimeline } = roomTimeline ?? {};
|
||||||
|
|
||||||
const className = ['message', (isBodyOnly ? 'message--body-only' : 'message--full')];
|
const className = ['message', isBodyOnly ? 'message--body-only' : 'message--full'];
|
||||||
if (focus) className.push('message--focus');
|
if (focus) className.push('message--focus');
|
||||||
const content = mEvent.getContent();
|
const content = mEvent.getContent();
|
||||||
const eventId = mEvent.getId();
|
const eventId = mEvent.getId();
|
||||||
|
@ -731,7 +745,8 @@ function Message({
|
||||||
const senderId = mEvent.getSender();
|
const senderId = mEvent.getSender();
|
||||||
let { body } = content;
|
let { body } = content;
|
||||||
const username = mEvent.sender ? getUsernameOfRoomMember(mEvent.sender) : getUsername(senderId);
|
const username = mEvent.sender ? getUsernameOfRoomMember(mEvent.sender) : getUsername(senderId);
|
||||||
const avatarSrc = mEvent.sender?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop') ?? null;
|
const avatarSrc =
|
||||||
|
mEvent.sender?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop') ?? null;
|
||||||
let isCustomHTML = content.format === 'org.matrix.custom.html';
|
let isCustomHTML = content.format === 'org.matrix.custom.html';
|
||||||
let customHTML = isCustomHTML ? content.formatted_body : null;
|
let customHTML = isCustomHTML ? content.formatted_body : null;
|
||||||
|
|
||||||
|
@ -765,18 +780,16 @@ function Message({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className.join(' ')}>
|
<div className={className.join(' ')}>
|
||||||
{
|
{isBodyOnly ? (
|
||||||
isBodyOnly
|
<div className="message__avatar-container" />
|
||||||
? <div className="message__avatar-container" />
|
) : (
|
||||||
: (
|
<MessageAvatar
|
||||||
<MessageAvatar
|
roomId={roomId}
|
||||||
roomId={roomId}
|
avatarSrc={avatarSrc}
|
||||||
avatarSrc={avatarSrc}
|
userId={senderId}
|
||||||
userId={senderId}
|
username={username}
|
||||||
username={username}
|
/>
|
||||||
/>
|
)}
|
||||||
)
|
|
||||||
}
|
|
||||||
<div className="message__main-container">
|
<div className="message__main-container">
|
||||||
{!isBodyOnly && (
|
{!isBodyOnly && (
|
||||||
<MessageHeader
|
<MessageHeader
|
||||||
|
@ -787,10 +800,7 @@ function Message({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{roomTimeline && isReply && (
|
{roomTimeline && isReply && (
|
||||||
<MessageReplyWrapper
|
<MessageReplyWrapper roomTimeline={roomTimeline} eventId={mEvent.replyEventId} />
|
||||||
roomTimeline={roomTimeline}
|
|
||||||
eventId={mEvent.replyEventId}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{!isEdit && (
|
{!isEdit && (
|
||||||
<MessageBody
|
<MessageBody
|
||||||
|
@ -803,9 +813,11 @@ function Message({
|
||||||
)}
|
)}
|
||||||
{isEdit && (
|
{isEdit && (
|
||||||
<MessageEdit
|
<MessageEdit
|
||||||
body={(customHTML
|
body={
|
||||||
? html(customHTML, { kind: 'edit', onlyPlain: true }).plain
|
customHTML
|
||||||
: plain(body, { kind: 'edit', onlyPlain: true }).plain)}
|
? html(customHTML, { kind: 'edit', onlyPlain: true }).plain
|
||||||
|
: plain(body, { kind: 'edit', onlyPlain: true }).plain
|
||||||
|
}
|
||||||
onSave={(newBody, oldBody) => {
|
onSave={(newBody, oldBody) => {
|
||||||
if (newBody !== oldBody) {
|
if (newBody !== oldBody) {
|
||||||
initMatrix.roomsInput.sendEditedMessage(roomId, mEvent, newBody);
|
initMatrix.roomsInput.sendEditedMessage(roomId, mEvent, newBody);
|
||||||
|
@ -815,16 +827,9 @@ function Message({
|
||||||
onCancel={cancelEdit}
|
onCancel={cancelEdit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{haveReactions && (
|
{haveReactions && <MessageReactionGroup roomTimeline={roomTimeline} mEvent={mEvent} />}
|
||||||
<MessageReactionGroup roomTimeline={roomTimeline} mEvent={mEvent} />
|
|
||||||
)}
|
|
||||||
{roomTimeline && !isEdit && (
|
{roomTimeline && !isEdit && (
|
||||||
<MessageOptions
|
<MessageOptions roomTimeline={roomTimeline} mEvent={mEvent} edit={edit} reply={reply} />
|
||||||
roomTimeline={roomTimeline}
|
|
||||||
mEvent={mEvent}
|
|
||||||
edit={edit}
|
|
||||||
reply={reply}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,16 +2,12 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './PeopleSelector.scss';
|
import './PeopleSelector.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import { blurOnBubbling } from '../../atoms/button/script';
|
import { blurOnBubbling } from '../../atoms/button/script';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import Avatar from '../../atoms/avatar/Avatar';
|
import Avatar from '../../atoms/avatar/Avatar';
|
||||||
|
|
||||||
function PeopleSelector({
|
function PeopleSelector({ avatarSrc, name, color, peopleRole, onClick }) {
|
||||||
avatarSrc, name, color, peopleRole, onClick,
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<div className="people-selector__container">
|
<div className="people-selector__container">
|
||||||
<button
|
<button
|
||||||
|
@ -21,8 +17,14 @@ function PeopleSelector({
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Avatar imageSrc={avatarSrc} text={name} bgColor={color} size="extra-small" />
|
<Avatar imageSrc={avatarSrc} text={name} bgColor={color} size="extra-small" />
|
||||||
<Text className="people-selector__name" variant="b1">{twemojify(name)}</Text>
|
<Text className="people-selector__name" variant="b1">
|
||||||
{peopleRole !== null && <Text className="people-selector__role" variant="b3">{peopleRole}</Text>}
|
{name}
|
||||||
|
</Text>
|
||||||
|
{peopleRole !== null && (
|
||||||
|
<Text className="people-selector__role" variant="b3">
|
||||||
|
{peopleRole}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,8 +2,6 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './PopupWindow.scss';
|
import './PopupWindow.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import IconButton from '../../atoms/button/IconButton';
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
import { MenuItem } from '../../atoms/context-menu/ContextMenu';
|
import { MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||||
|
@ -13,19 +11,11 @@ import RawModal from '../../atoms/modal/RawModal';
|
||||||
|
|
||||||
import ChevronLeftIC from '../../../../public/res/ic/outlined/chevron-left.svg';
|
import ChevronLeftIC from '../../../../public/res/ic/outlined/chevron-left.svg';
|
||||||
|
|
||||||
function PWContentSelector({
|
function PWContentSelector({ selected, variant, iconSrc, type, onClick, children }) {
|
||||||
selected, variant, iconSrc,
|
|
||||||
type, onClick, children,
|
|
||||||
}) {
|
|
||||||
const pwcsClass = selected ? ' pw-content-selector--selected' : '';
|
const pwcsClass = selected ? ' pw-content-selector--selected' : '';
|
||||||
return (
|
return (
|
||||||
<div className={`pw-content-selector${pwcsClass}`}>
|
<div className={`pw-content-selector${pwcsClass}`}>
|
||||||
<MenuItem
|
<MenuItem variant={variant} iconSrc={iconSrc} type={type} onClick={onClick}>
|
||||||
variant={variant}
|
|
||||||
iconSrc={iconSrc}
|
|
||||||
type={type}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
|
@ -49,9 +39,16 @@ PWContentSelector.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function PopupWindow({
|
function PopupWindow({
|
||||||
className, isOpen, title, contentTitle,
|
className,
|
||||||
drawer, drawerOptions, contentOptions,
|
isOpen,
|
||||||
onAfterClose, onRequestClose, children,
|
title,
|
||||||
|
contentTitle,
|
||||||
|
drawer,
|
||||||
|
drawerOptions,
|
||||||
|
contentOptions,
|
||||||
|
onAfterClose,
|
||||||
|
onRequestClose,
|
||||||
|
children,
|
||||||
}) {
|
}) {
|
||||||
const haveDrawer = drawer !== null;
|
const haveDrawer = drawer !== null;
|
||||||
const cTitle = contentTitle !== null ? contentTitle : title;
|
const cTitle = contentTitle !== null ? contentTitle : title;
|
||||||
|
@ -69,21 +66,26 @@ function PopupWindow({
|
||||||
{haveDrawer && (
|
{haveDrawer && (
|
||||||
<div className="pw__drawer">
|
<div className="pw__drawer">
|
||||||
<Header>
|
<Header>
|
||||||
<IconButton size="small" src={ChevronLeftIC} onClick={onRequestClose} tooltip="Back" />
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
src={ChevronLeftIC}
|
||||||
|
onClick={onRequestClose}
|
||||||
|
tooltip="Back"
|
||||||
|
/>
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
{
|
{typeof title === 'string' ? (
|
||||||
typeof title === 'string'
|
<Text variant="s1" weight="medium" primary>
|
||||||
? <Text variant="s1" weight="medium" primary>{twemojify(title)}</Text>
|
{title}
|
||||||
: title
|
</Text>
|
||||||
}
|
) : (
|
||||||
|
title
|
||||||
|
)}
|
||||||
</TitleWrapper>
|
</TitleWrapper>
|
||||||
{drawerOptions}
|
{drawerOptions}
|
||||||
</Header>
|
</Header>
|
||||||
<div className="pw__drawer__content__wrapper">
|
<div className="pw__drawer__content__wrapper">
|
||||||
<ScrollView invisible>
|
<ScrollView invisible>
|
||||||
<div className="pw__drawer__content">
|
<div className="pw__drawer__content">{drawer}</div>
|
||||||
{drawer}
|
|
||||||
</div>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -91,19 +93,19 @@ function PopupWindow({
|
||||||
<div className="pw__content">
|
<div className="pw__content">
|
||||||
<Header>
|
<Header>
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
{
|
{typeof cTitle === 'string' ? (
|
||||||
typeof cTitle === 'string'
|
<Text variant="h2" weight="medium" primary>
|
||||||
? <Text variant="h2" weight="medium" primary>{twemojify(cTitle)}</Text>
|
{cTitle}
|
||||||
: cTitle
|
</Text>
|
||||||
}
|
) : (
|
||||||
|
cTitle
|
||||||
|
)}
|
||||||
</TitleWrapper>
|
</TitleWrapper>
|
||||||
{contentOptions}
|
{contentOptions}
|
||||||
</Header>
|
</Header>
|
||||||
<div className="pw__content__wrapper">
|
<div className="pw__content__wrapper">
|
||||||
<ScrollView autoHide>
|
<ScrollView autoHide>
|
||||||
<div className="pw__content-container">
|
<div className="pw__content-container">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import { openInviteUser } from '../../../client/action/navigation';
|
import { openInviteUser } from '../../../client/action/navigation';
|
||||||
import * as roomActions from '../../../client/action/room';
|
import * as roomActions from '../../../client/action/room';
|
||||||
|
@ -37,7 +35,7 @@ function RoomOptions({ roomId, afterOptionSelect }) {
|
||||||
'Leave room',
|
'Leave room',
|
||||||
`Are you sure that you want to leave "${room.name}" room?`,
|
`Are you sure that you want to leave "${room.name}" room?`,
|
||||||
'Leave',
|
'Leave',
|
||||||
'danger',
|
'danger'
|
||||||
);
|
);
|
||||||
if (!isConfirmed) return;
|
if (!isConfirmed) return;
|
||||||
roomActions.leave(roomId);
|
roomActions.leave(roomId);
|
||||||
|
@ -45,16 +43,16 @@ function RoomOptions({ roomId, afterOptionSelect }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ maxWidth: '256px' }}>
|
<div style={{ maxWidth: '256px' }}>
|
||||||
<MenuHeader>{twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)}</MenuHeader>
|
<MenuHeader>{`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`}</MenuHeader>
|
||||||
<MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>Mark as read</MenuItem>
|
<MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>
|
||||||
<MenuItem
|
Mark as read
|
||||||
iconSrc={AddUserIC}
|
</MenuItem>
|
||||||
onClick={handleInviteClick}
|
<MenuItem iconSrc={AddUserIC} onClick={handleInviteClick} disabled={!canInvite}>
|
||||||
disabled={!canInvite}
|
|
||||||
>
|
|
||||||
Invite
|
Invite
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem iconSrc={LeaveArrowIC} variant="danger" onClick={handleLeaveClick}>Leave</MenuItem>
|
<MenuItem iconSrc={LeaveArrowIC} variant="danger" onClick={handleLeaveClick}>
|
||||||
|
Leave
|
||||||
|
</MenuItem>
|
||||||
<MenuHeader>Notification</MenuHeader>
|
<MenuHeader>Notification</MenuHeader>
|
||||||
<RoomNotification roomId={roomId} />
|
<RoomNotification roomId={roomId} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,8 +2,6 @@ import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './RoomProfile.scss';
|
import './RoomProfile.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
|
@ -33,7 +31,9 @@ function RoomProfile({ roomId }) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const isDM = initMatrix.roomList.directs.has(roomId);
|
const isDM = initMatrix.roomList.directs.has(roomId);
|
||||||
let avatarSrc = mx.getRoom(roomId).getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
|
let avatarSrc = mx.getRoom(roomId).getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
|
||||||
avatarSrc = isDM ? mx.getRoom(roomId).getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop') : avatarSrc;
|
avatarSrc = isDM
|
||||||
|
? mx.getRoom(roomId).getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop')
|
||||||
|
: avatarSrc;
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
const { currentState } = room;
|
const { currentState } = room;
|
||||||
const roomName = room.name;
|
const roomName = room.name;
|
||||||
|
@ -122,7 +122,7 @@ function RoomProfile({ roomId }) {
|
||||||
'Remove avatar',
|
'Remove avatar',
|
||||||
'Are you sure that you want to remove room avatar?',
|
'Are you sure that you want to remove room avatar?',
|
||||||
'Remove',
|
'Remove',
|
||||||
'caution',
|
'caution'
|
||||||
);
|
);
|
||||||
if (isConfirmed) {
|
if (isConfirmed) {
|
||||||
await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
|
await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
|
||||||
|
@ -132,15 +132,45 @@ function RoomProfile({ roomId }) {
|
||||||
|
|
||||||
const renderEditNameAndTopic = () => (
|
const renderEditNameAndTopic = () => (
|
||||||
<form className="room-profile__edit-form" onSubmit={handleOnSubmit}>
|
<form className="room-profile__edit-form" onSubmit={handleOnSubmit}>
|
||||||
{canChangeName && <Input value={roomName} name="room-name" disabled={status.type === cons.status.IN_FLIGHT} label="Name" />}
|
{canChangeName && (
|
||||||
{canChangeTopic && <Input value={roomTopic} name="room-topic" disabled={status.type === cons.status.IN_FLIGHT} minHeight={100} resizable label="Topic" />}
|
<Input
|
||||||
{(!canChangeName || !canChangeTopic) && <Text variant="b3">{`You have permission to change ${room.isSpaceRoom() ? 'space' : 'room'} ${canChangeName ? 'name' : 'topic'} only.`}</Text>}
|
value={roomName}
|
||||||
{ status.type === cons.status.IN_FLIGHT && <Text variant="b2">{status.msg}</Text>}
|
name="room-name"
|
||||||
{ status.type === cons.status.SUCCESS && <Text style={{ color: 'var(--tc-positive-high)' }} variant="b2">{status.msg}</Text>}
|
disabled={status.type === cons.status.IN_FLIGHT}
|
||||||
{ status.type === cons.status.ERROR && <Text style={{ color: 'var(--tc-danger-high)' }} variant="b2">{status.msg}</Text>}
|
label="Name"
|
||||||
{ status.type !== cons.status.IN_FLIGHT && (
|
/>
|
||||||
|
)}
|
||||||
|
{canChangeTopic && (
|
||||||
|
<Input
|
||||||
|
value={roomTopic}
|
||||||
|
name="room-topic"
|
||||||
|
disabled={status.type === cons.status.IN_FLIGHT}
|
||||||
|
minHeight={100}
|
||||||
|
resizable
|
||||||
|
label="Topic"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(!canChangeName || !canChangeTopic) && (
|
||||||
|
<Text variant="b3">{`You have permission to change ${
|
||||||
|
room.isSpaceRoom() ? 'space' : 'room'
|
||||||
|
} ${canChangeName ? 'name' : 'topic'} only.`}</Text>
|
||||||
|
)}
|
||||||
|
{status.type === cons.status.IN_FLIGHT && <Text variant="b2">{status.msg}</Text>}
|
||||||
|
{status.type === cons.status.SUCCESS && (
|
||||||
|
<Text style={{ color: 'var(--tc-positive-high)' }} variant="b2">
|
||||||
|
{status.msg}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{status.type === cons.status.ERROR && (
|
||||||
|
<Text style={{ color: 'var(--tc-danger-high)' }} variant="b2">
|
||||||
|
{status.msg}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{status.type !== cons.status.IN_FLIGHT && (
|
||||||
<div>
|
<div>
|
||||||
<Button type="submit" variant="primary">Save</Button>
|
<Button type="submit" variant="primary">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
<Button onClick={handleCancelEditing}>Cancel</Button>
|
<Button onClick={handleCancelEditing}>Cancel</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -148,10 +178,15 @@ function RoomProfile({ roomId }) {
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderNameAndTopic = () => (
|
const renderNameAndTopic = () => (
|
||||||
<div className="room-profile__display" style={{ marginBottom: avatarSrc && canChangeAvatar ? '24px' : '0' }}>
|
<div
|
||||||
|
className="room-profile__display"
|
||||||
|
style={{ marginBottom: avatarSrc && canChangeAvatar ? '24px' : '0' }}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<Text variant="h2" weight="medium" primary>{twemojify(roomName)}</Text>
|
<Text variant="h2" weight="medium" primary>
|
||||||
{ (canChangeName || canChangeTopic) && (
|
{roomName}
|
||||||
|
</Text>
|
||||||
|
{(canChangeName || canChangeTopic) && (
|
||||||
<IconButton
|
<IconButton
|
||||||
src={PencilIC}
|
src={PencilIC}
|
||||||
size="extra-small"
|
size="extra-small"
|
||||||
|
@ -161,15 +196,17 @@ function RoomProfile({ roomId }) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Text variant="b3">{room.getCanonicalAlias() || room.roomId}</Text>
|
<Text variant="b3">{room.getCanonicalAlias() || room.roomId}</Text>
|
||||||
{roomTopic && <Text variant="b2">{twemojify(roomTopic, undefined, true)}</Text>}
|
{roomTopic && <Text variant="b2">{roomTopic}</Text>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="room-profile">
|
<div className="room-profile">
|
||||||
<div className="room-profile__content">
|
<div className="room-profile__content">
|
||||||
{ !canChangeAvatar && <Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="large" />}
|
{!canChangeAvatar && (
|
||||||
{ canChangeAvatar && (
|
<Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="large" />
|
||||||
|
)}
|
||||||
|
{canChangeAvatar && (
|
||||||
<ImageUpload
|
<ImageUpload
|
||||||
text={roomName}
|
text={roomName}
|
||||||
bgColor={colorMXID(roomId)}
|
bgColor={colorMXID(roomId)}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './RoomSelector.scss';
|
import './RoomSelector.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
|
@ -11,8 +10,13 @@ import NotificationBadge from '../../atoms/badge/NotificationBadge';
|
||||||
import { blurOnBubbling } from '../../atoms/button/script';
|
import { blurOnBubbling } from '../../atoms/button/script';
|
||||||
|
|
||||||
function RoomSelectorWrapper({
|
function RoomSelectorWrapper({
|
||||||
isSelected, isMuted, isUnread, onClick,
|
isSelected,
|
||||||
content, options, onContextMenu,
|
isMuted,
|
||||||
|
isUnread,
|
||||||
|
onClick,
|
||||||
|
content,
|
||||||
|
options,
|
||||||
|
onContextMenu,
|
||||||
}) {
|
}) {
|
||||||
const classes = ['room-selector'];
|
const classes = ['room-selector'];
|
||||||
if (isMuted) classes.push('room-selector--muted');
|
if (isMuted) classes.push('room-selector--muted');
|
||||||
|
@ -50,16 +54,26 @@ RoomSelectorWrapper.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function RoomSelector({
|
function RoomSelector({
|
||||||
name, parentName, roomId, imageSrc, iconSrc,
|
name,
|
||||||
isSelected, isMuted, isUnread, notificationCount, isAlert,
|
parentName,
|
||||||
options, onClick, onContextMenu,
|
roomId,
|
||||||
|
imageSrc,
|
||||||
|
iconSrc,
|
||||||
|
isSelected,
|
||||||
|
isMuted,
|
||||||
|
isUnread,
|
||||||
|
notificationCount,
|
||||||
|
isAlert,
|
||||||
|
options,
|
||||||
|
onClick,
|
||||||
|
onContextMenu,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<RoomSelectorWrapper
|
<RoomSelectorWrapper
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
isMuted={isMuted}
|
isMuted={isMuted}
|
||||||
isUnread={isUnread}
|
isUnread={isUnread}
|
||||||
content={(
|
content={
|
||||||
<>
|
<>
|
||||||
<Avatar
|
<Avatar
|
||||||
text={name}
|
text={name}
|
||||||
|
@ -70,22 +84,22 @@ function RoomSelector({
|
||||||
size="extra-small"
|
size="extra-small"
|
||||||
/>
|
/>
|
||||||
<Text variant="b1" weight={isUnread ? 'medium' : 'normal'}>
|
<Text variant="b1" weight={isUnread ? 'medium' : 'normal'}>
|
||||||
{twemojify(name)}
|
{name}
|
||||||
{parentName && (
|
{parentName && (
|
||||||
<Text variant="b3" span>
|
<Text variant="b3" span>
|
||||||
{' — '}
|
{' — '}
|
||||||
{twemojify(parentName)}
|
{parentName}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
{ isUnread && (
|
{isUnread && (
|
||||||
<NotificationBadge
|
<NotificationBadge
|
||||||
alert={isAlert}
|
alert={isAlert}
|
||||||
content={notificationCount !== 0 ? notificationCount : null}
|
content={notificationCount !== 0 ? notificationCount : null}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
}
|
||||||
options={options}
|
options={options}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
|
@ -110,10 +124,7 @@ RoomSelector.propTypes = {
|
||||||
isSelected: PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
isMuted: PropTypes.bool,
|
isMuted: PropTypes.bool,
|
||||||
isUnread: PropTypes.bool.isRequired,
|
isUnread: PropTypes.bool.isRequired,
|
||||||
notificationCount: PropTypes.oneOfType([
|
notificationCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.number,
|
|
||||||
]).isRequired,
|
|
||||||
isAlert: PropTypes.bool.isRequired,
|
isAlert: PropTypes.bool.isRequired,
|
||||||
options: PropTypes.node,
|
options: PropTypes.node,
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -2,46 +2,35 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './RoomTile.scss';
|
import './RoomTile.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import Avatar from '../../atoms/avatar/Avatar';
|
import Avatar from '../../atoms/avatar/Avatar';
|
||||||
|
|
||||||
function RoomTile({
|
function RoomTile({ avatarSrc, name, id, inviterName, memberCount, desc, options }) {
|
||||||
avatarSrc, name, id,
|
|
||||||
inviterName, memberCount, desc, options,
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<div className="room-tile">
|
<div className="room-tile">
|
||||||
<div className="room-tile__avatar">
|
<div className="room-tile__avatar">
|
||||||
<Avatar
|
<Avatar imageSrc={avatarSrc} bgColor={colorMXID(id)} text={name} />
|
||||||
imageSrc={avatarSrc}
|
|
||||||
bgColor={colorMXID(id)}
|
|
||||||
text={name}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="room-tile__content">
|
<div className="room-tile__content">
|
||||||
<Text variant="s1">{twemojify(name)}</Text>
|
<Text variant="s1">{name}</Text>
|
||||||
<Text variant="b3">
|
<Text variant="b3">
|
||||||
{
|
{inviterName !== null
|
||||||
inviterName !== null
|
? `Invited by ${inviterName} to ${id}${
|
||||||
? `Invited by ${inviterName} to ${id}${memberCount === null ? '' : ` • ${memberCount} members`}`
|
memberCount === null ? '' : ` • ${memberCount} members`
|
||||||
: id + (memberCount === null ? '' : ` • ${memberCount} members`)
|
}`
|
||||||
}
|
: id + (memberCount === null ? '' : ` • ${memberCount} members`)}
|
||||||
</Text>
|
</Text>
|
||||||
{
|
{desc !== null && typeof desc === 'string' ? (
|
||||||
desc !== null && (typeof desc === 'string')
|
<Text className="room-tile__content__desc" variant="b2">
|
||||||
? <Text className="room-tile__content__desc" variant="b2">{twemojify(desc, undefined, true)}</Text>
|
{desc}
|
||||||
: desc
|
</Text>
|
||||||
}
|
) : (
|
||||||
|
desc
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{ options !== null && (
|
{options !== null && <div className="room-tile__options">{options}</div>}
|
||||||
<div className="room-tile__options">
|
|
||||||
{options}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -58,10 +47,7 @@ RoomTile.propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
inviterName: PropTypes.string,
|
inviterName: PropTypes.string,
|
||||||
memberCount: PropTypes.oneOfType([
|
memberCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.number,
|
|
||||||
]),
|
|
||||||
desc: PropTypes.node,
|
desc: PropTypes.node,
|
||||||
options: PropTypes.node,
|
options: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,38 +2,32 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './SidebarAvatar.scss';
|
import './SidebarAvatar.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import Tooltip from '../../atoms/tooltip/Tooltip';
|
import Tooltip from '../../atoms/tooltip/Tooltip';
|
||||||
import { blurOnBubbling } from '../../atoms/button/script';
|
import { blurOnBubbling } from '../../atoms/button/script';
|
||||||
|
|
||||||
const SidebarAvatar = React.forwardRef(({
|
const SidebarAvatar = React.forwardRef(
|
||||||
className, tooltip, active, onClick,
|
({ className, tooltip, active, onClick, onContextMenu, avatar, notificationBadge }, ref) => {
|
||||||
onContextMenu, avatar, notificationBadge,
|
const classes = ['sidebar-avatar'];
|
||||||
}, ref) => {
|
if (active) classes.push('sidebar-avatar--active');
|
||||||
const classes = ['sidebar-avatar'];
|
if (className) classes.push(className);
|
||||||
if (active) classes.push('sidebar-avatar--active');
|
return (
|
||||||
if (className) classes.push(className);
|
<Tooltip content={<Text variant="b1">{tooltip}</Text>} placement="right">
|
||||||
return (
|
<button
|
||||||
<Tooltip
|
ref={ref}
|
||||||
content={<Text variant="b1">{twemojify(tooltip)}</Text>}
|
className={classes.join(' ')}
|
||||||
placement="right"
|
type="button"
|
||||||
>
|
onMouseUp={(e) => blurOnBubbling(e, '.sidebar-avatar')}
|
||||||
<button
|
onClick={onClick}
|
||||||
ref={ref}
|
onContextMenu={onContextMenu}
|
||||||
className={classes.join(' ')}
|
>
|
||||||
type="button"
|
{avatar}
|
||||||
onMouseUp={(e) => blurOnBubbling(e, '.sidebar-avatar')}
|
{notificationBadge}
|
||||||
onClick={onClick}
|
</button>
|
||||||
onContextMenu={onContextMenu}
|
</Tooltip>
|
||||||
>
|
);
|
||||||
{avatar}
|
}
|
||||||
{notificationBadge}
|
);
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
SidebarAvatar.defaultProps = {
|
SidebarAvatar.defaultProps = {
|
||||||
className: null,
|
className: null,
|
||||||
active: false,
|
active: false,
|
||||||
|
|
|
@ -2,8 +2,6 @@ import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './SpaceAddExisting.scss';
|
import './SpaceAddExisting.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
|
@ -33,14 +31,12 @@ function SpaceAddExistingContent({ roomId }) {
|
||||||
const [selected, setSelected] = useState([]);
|
const [selected, setSelected] = useState([]);
|
||||||
const [searchIds, setSearchIds] = useState(null);
|
const [searchIds, setSearchIds] = useState(null);
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const {
|
const { spaces, rooms, directs, roomIdToParents } = initMatrix.roomList;
|
||||||
spaces, rooms, directs, roomIdToParents,
|
|
||||||
} = initMatrix.roomList;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const allIds = [...spaces, ...rooms, ...directs].filter((rId) => (
|
const allIds = [...spaces, ...rooms, ...directs].filter(
|
||||||
rId !== roomId && !roomIdToParents.get(rId)?.has(roomId)
|
(rId) => rId !== roomId && !roomIdToParents.get(rId)?.has(roomId)
|
||||||
));
|
);
|
||||||
setAllRoomIds(allIds);
|
setAllRoomIds(allIds);
|
||||||
}, [roomId]);
|
}, [roomId]);
|
||||||
|
|
||||||
|
@ -68,20 +64,25 @@ function SpaceAddExistingContent({ roomId }) {
|
||||||
via.push(getIdServer(rId));
|
via.push(getIdServer(rId));
|
||||||
}
|
}
|
||||||
|
|
||||||
return mx.sendStateEvent(roomId, 'm.space.child', {
|
return mx.sendStateEvent(
|
||||||
auto_join: false,
|
roomId,
|
||||||
suggested: false,
|
'm.space.child',
|
||||||
via,
|
{
|
||||||
}, rId);
|
auto_join: false,
|
||||||
|
suggested: false,
|
||||||
|
via,
|
||||||
|
},
|
||||||
|
rId
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
mountStore.setItem(true);
|
mountStore.setItem(true);
|
||||||
await Promise.allSettled(promises);
|
await Promise.allSettled(promises);
|
||||||
if (mountStore.getItem() !== true) return;
|
if (mountStore.getItem() !== true) return;
|
||||||
|
|
||||||
const allIds = [...spaces, ...rooms, ...directs].filter((rId) => (
|
const allIds = [...spaces, ...rooms, ...directs].filter(
|
||||||
rId !== roomId && !roomIdToParents.get(rId)?.has(roomId) && !selected.includes(rId)
|
(rId) => rId !== roomId && !roomIdToParents.get(rId)?.has(roomId) && !selected.includes(rId)
|
||||||
));
|
);
|
||||||
setAllRoomIds(allIds);
|
setAllRoomIds(allIds);
|
||||||
setProcess(null);
|
setProcess(null);
|
||||||
setSelected([]);
|
setSelected([]);
|
||||||
|
@ -98,9 +99,7 @@ function SpaceAddExistingContent({ roomId }) {
|
||||||
const searchedIds = allRoomIds.filter((rId) => {
|
const searchedIds = allRoomIds.filter((rId) => {
|
||||||
let name = mx.getRoom(rId)?.name;
|
let name = mx.getRoom(rId)?.name;
|
||||||
if (!name) return false;
|
if (!name) return false;
|
||||||
name = name.normalize('NFKC')
|
name = name.normalize('NFKC').toLocaleLowerCase().replace(/\s/g, '');
|
||||||
.toLocaleLowerCase()
|
|
||||||
.replace(/\s/g, '');
|
|
||||||
return name.includes(term);
|
return name.includes(term);
|
||||||
});
|
});
|
||||||
setSearchIds(searchedIds);
|
setSearchIds(searchedIds);
|
||||||
|
@ -114,66 +113,64 @@ function SpaceAddExistingContent({ roomId }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form onSubmit={(ev) => { ev.preventDefault(); }}>
|
<form
|
||||||
|
onSubmit={(ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<RawIcon size="small" src={SearchIC} />
|
<RawIcon size="small" src={SearchIC} />
|
||||||
<Input
|
<Input name="searchInput" onChange={handleSearch} placeholder="Search room" autoFocus />
|
||||||
name="searchInput"
|
|
||||||
onChange={handleSearch}
|
|
||||||
placeholder="Search room"
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<IconButton size="small" type="button" onClick={handleSearchClear} src={CrossIC} />
|
<IconButton size="small" type="button" onClick={handleSearchClear} src={CrossIC} />
|
||||||
</form>
|
</form>
|
||||||
{searchIds?.length === 0 && <Text>No results found</Text>}
|
{searchIds?.length === 0 && <Text>No results found</Text>}
|
||||||
{
|
{(searchIds || allRoomIds).map((rId) => {
|
||||||
(searchIds || allRoomIds).map((rId) => {
|
const room = mx.getRoom(rId);
|
||||||
const room = mx.getRoom(rId);
|
let imageSrc =
|
||||||
let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
|
room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
|
||||||
if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
|
if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
|
||||||
|
|
||||||
const parentSet = roomIdToParents.get(rId);
|
const parentSet = roomIdToParents.get(rId);
|
||||||
const parentNames = parentSet
|
const parentNames = parentSet
|
||||||
? [...parentSet].map((parentId) => mx.getRoom(parentId).name)
|
? [...parentSet].map((parentId) => mx.getRoom(parentId).name)
|
||||||
: undefined;
|
: undefined;
|
||||||
const parents = parentNames ? parentNames.join(', ') : null;
|
const parents = parentNames ? parentNames.join(', ') : null;
|
||||||
|
|
||||||
const handleSelect = () => toggleSelection(rId);
|
const handleSelect = () => toggleSelection(rId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RoomSelector
|
<RoomSelector
|
||||||
key={rId}
|
key={rId}
|
||||||
name={room.name}
|
name={room.name}
|
||||||
parentName={parents}
|
parentName={parents}
|
||||||
roomId={rId}
|
roomId={rId}
|
||||||
imageSrc={directs.has(rId) ? imageSrc : null}
|
imageSrc={directs.has(rId) ? imageSrc : null}
|
||||||
iconSrc={
|
iconSrc={
|
||||||
directs.has(rId)
|
directs.has(rId) ? null : joinRuleToIconSrc(room.getJoinRule(), room.isSpaceRoom())
|
||||||
? null
|
}
|
||||||
: joinRuleToIconSrc(room.getJoinRule(), room.isSpaceRoom())
|
isUnread={false}
|
||||||
}
|
notificationCount={0}
|
||||||
isUnread={false}
|
isAlert={false}
|
||||||
notificationCount={0}
|
onClick={handleSelect}
|
||||||
isAlert={false}
|
options={
|
||||||
onClick={handleSelect}
|
<Checkbox
|
||||||
options={(
|
isActive={selected.includes(rId)}
|
||||||
<Checkbox
|
variant="positive"
|
||||||
isActive={selected.includes(rId)}
|
onToggle={handleSelect}
|
||||||
variant="positive"
|
tabIndex={-1}
|
||||||
onToggle={handleSelect}
|
disabled={process !== null}
|
||||||
tabIndex={-1}
|
/>
|
||||||
disabled={process !== null}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
/>
|
})}
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
{selected.length !== 0 && (
|
{selected.length !== 0 && (
|
||||||
<div className="space-add-existing__footer">
|
<div className="space-add-existing__footer">
|
||||||
{process && <Spinner size="small" />}
|
{process && <Spinner size="small" />}
|
||||||
<Text weight="medium">{process || `${selected.length} item selected`}</Text>
|
<Text weight="medium">{process || `${selected.length} item selected`}</Text>
|
||||||
{ !process && (
|
{!process && (
|
||||||
<Button onClick={handleAdd} variant="primary">Add</Button>
|
<Button onClick={handleAdd} variant="primary">
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -209,20 +206,16 @@ function SpaceAddExisting() {
|
||||||
<Dialog
|
<Dialog
|
||||||
isOpen={roomId !== null}
|
isOpen={roomId !== null}
|
||||||
className="space-add-existing"
|
className="space-add-existing"
|
||||||
title={(
|
title={
|
||||||
<Text variant="s1" weight="medium" primary>
|
<Text variant="s1" weight="medium" primary>
|
||||||
{roomId && twemojify(room.name)}
|
{roomId && room.name}
|
||||||
<span style={{ color: 'var(--tc-surface-low)' }}> — add existing rooms</span>
|
<span style={{ color: 'var(--tc-surface-low)' }}> — add existing rooms</span>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
}
|
||||||
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
|
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
|
||||||
onRequestClose={requestClose}
|
onRequestClose={requestClose}
|
||||||
>
|
>
|
||||||
{
|
{roomId ? <SpaceAddExistingContent roomId={roomId} /> : <div />}
|
||||||
roomId
|
|
||||||
? <SpaceAddExistingContent roomId={roomId} />
|
|
||||||
: <div />
|
|
||||||
}
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import { openSpaceSettings, openSpaceManage, openInviteUser } from '../../../client/action/navigation';
|
import {
|
||||||
|
openSpaceSettings,
|
||||||
|
openSpaceManage,
|
||||||
|
openInviteUser,
|
||||||
|
} from '../../../client/action/navigation';
|
||||||
import { markAsRead } from '../../../client/action/notifications';
|
import { markAsRead } from '../../../client/action/notifications';
|
||||||
import { leave } from '../../../client/action/room';
|
import { leave } from '../../../client/action/room';
|
||||||
import {
|
import {
|
||||||
|
@ -74,7 +76,7 @@ function SpaceOptions({ roomId, afterOptionSelect }) {
|
||||||
'Leave space',
|
'Leave space',
|
||||||
`Are you sure that you want to leave "${room.name}" space?`,
|
`Are you sure that you want to leave "${room.name}" space?`,
|
||||||
'Leave',
|
'Leave',
|
||||||
'danger',
|
'danger'
|
||||||
);
|
);
|
||||||
if (!isConfirmed) return;
|
if (!isConfirmed) return;
|
||||||
leave(roomId);
|
leave(roomId);
|
||||||
|
@ -82,34 +84,29 @@ function SpaceOptions({ roomId, afterOptionSelect }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ maxWidth: 'calc(var(--navigation-drawer-width) - var(--sp-normal))' }}>
|
<div style={{ maxWidth: 'calc(var(--navigation-drawer-width) - var(--sp-normal))' }}>
|
||||||
<MenuHeader>{twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)}</MenuHeader>
|
<MenuHeader>{`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`}</MenuHeader>
|
||||||
<MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>Mark as read</MenuItem>
|
<MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>
|
||||||
|
Mark as read
|
||||||
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={handleCategorizeClick}
|
onClick={handleCategorizeClick}
|
||||||
iconSrc={isCategorized ? CategoryFilledIC : CategoryIC}
|
iconSrc={isCategorized ? CategoryFilledIC : CategoryIC}
|
||||||
>
|
>
|
||||||
{isCategorized ? 'Uncategorize subspaces' : 'Categorize subspaces'}
|
{isCategorized ? 'Uncategorize subspaces' : 'Categorize subspaces'}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem onClick={handlePinClick} iconSrc={isPinned ? PinFilledIC : PinIC}>
|
||||||
onClick={handlePinClick}
|
|
||||||
iconSrc={isPinned ? PinFilledIC : PinIC}
|
|
||||||
>
|
|
||||||
{isPinned ? 'Unpin from sidebar' : 'Pin to sidebar'}
|
{isPinned ? 'Unpin from sidebar' : 'Pin to sidebar'}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem iconSrc={AddUserIC} onClick={handleInviteClick} disabled={!canInvite}>
|
||||||
iconSrc={AddUserIC}
|
|
||||||
onClick={handleInviteClick}
|
|
||||||
disabled={!canInvite}
|
|
||||||
>
|
|
||||||
Invite
|
Invite
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={handleManageRoom} iconSrc={HashSearchIC}>Manage rooms</MenuItem>
|
<MenuItem onClick={handleManageRoom} iconSrc={HashSearchIC}>
|
||||||
<MenuItem onClick={handleSettingsClick} iconSrc={SettingsIC}>Settings</MenuItem>
|
Manage rooms
|
||||||
<MenuItem
|
</MenuItem>
|
||||||
variant="danger"
|
<MenuItem onClick={handleSettingsClick} iconSrc={SettingsIC}>
|
||||||
onClick={handleLeaveClick}
|
Settings
|
||||||
iconSrc={LeaveArrowIC}
|
</MenuItem>
|
||||||
>
|
<MenuItem variant="danger" onClick={handleLeaveClick} iconSrc={LeaveArrowIC}>
|
||||||
Leave
|
Leave
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,6 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './CreateRoom.scss';
|
import './CreateRoom.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
|
@ -92,7 +91,7 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
|
||||||
topic,
|
topic,
|
||||||
joinRule,
|
joinRule,
|
||||||
alias: roomAlias,
|
alias: roomAlias,
|
||||||
isEncrypted: (isSpace || joinRule === 'public') ? false : isEncrypted,
|
isEncrypted: isSpace || joinRule === 'public' ? false : isEncrypted,
|
||||||
powerLevel,
|
powerLevel,
|
||||||
isSpace,
|
isSpace,
|
||||||
parentId,
|
parentId,
|
||||||
|
@ -131,36 +130,35 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
|
||||||
|
|
||||||
const joinRules = ['invite', 'restricted', 'public'];
|
const joinRules = ['invite', 'restricted', 'public'];
|
||||||
const joinRuleShortText = ['Private', 'Restricted', 'Public'];
|
const joinRuleShortText = ['Private', 'Restricted', 'Public'];
|
||||||
const joinRuleText = ['Private (invite only)', 'Restricted (space member can join)', 'Public (anyone can join)'];
|
const joinRuleText = [
|
||||||
|
'Private (invite only)',
|
||||||
|
'Restricted (space member can join)',
|
||||||
|
'Public (anyone can join)',
|
||||||
|
];
|
||||||
const jrRoomIC = [HashLockIC, HashIC, HashGlobeIC];
|
const jrRoomIC = [HashLockIC, HashIC, HashGlobeIC];
|
||||||
const jrSpaceIC = [SpaceLockIC, SpaceIC, SpaceGlobeIC];
|
const jrSpaceIC = [SpaceLockIC, SpaceIC, SpaceGlobeIC];
|
||||||
const handleJoinRule = (evt) => {
|
const handleJoinRule = (evt) => {
|
||||||
openReusableContextMenu(
|
openReusableContextMenu('bottom', getEventCords(evt, '.btn-surface'), (closeMenu) => (
|
||||||
'bottom',
|
<>
|
||||||
getEventCords(evt, '.btn-surface'),
|
<MenuHeader>Visibility (who can join)</MenuHeader>
|
||||||
(closeMenu) => (
|
{joinRules.map((rule) => (
|
||||||
<>
|
<MenuItem
|
||||||
<MenuHeader>Visibility (who can join)</MenuHeader>
|
key={rule}
|
||||||
{
|
variant={rule === joinRule ? 'positive' : 'surface'}
|
||||||
joinRules.map((rule) => (
|
iconSrc={
|
||||||
<MenuItem
|
isSpace ? jrSpaceIC[joinRules.indexOf(rule)] : jrRoomIC[joinRules.indexOf(rule)]
|
||||||
key={rule}
|
}
|
||||||
variant={rule === joinRule ? 'positive' : 'surface'}
|
onClick={() => {
|
||||||
iconSrc={
|
closeMenu();
|
||||||
isSpace
|
setJoinRule(rule);
|
||||||
? jrSpaceIC[joinRules.indexOf(rule)]
|
}}
|
||||||
: jrRoomIC[joinRules.indexOf(rule)]
|
disabled={!parentId && rule === 'restricted'}
|
||||||
}
|
>
|
||||||
onClick={() => { closeMenu(); setJoinRule(rule); }}
|
{joinRuleText[joinRules.indexOf(rule)]}
|
||||||
disabled={!parentId && rule === 'restricted'}
|
</MenuItem>
|
||||||
>
|
))}
|
||||||
{ joinRuleText[joinRules.indexOf(rule)] }
|
</>
|
||||||
</MenuItem>
|
));
|
||||||
))
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -168,50 +166,64 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
|
||||||
<form className="create-room__form" onSubmit={handleSubmit}>
|
<form className="create-room__form" onSubmit={handleSubmit}>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Visibility"
|
title="Visibility"
|
||||||
options={(
|
options={
|
||||||
<Button onClick={handleJoinRule} iconSrc={ChevronBottomIC}>
|
<Button onClick={handleJoinRule} iconSrc={ChevronBottomIC}>
|
||||||
{joinRuleShortText[joinRules.indexOf(joinRule)]}
|
{joinRuleShortText[joinRules.indexOf(joinRule)]}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
}
|
||||||
content={<Text variant="b3">{`Select who can join this ${isSpace ? 'space' : 'room'}.`}</Text>}
|
content={
|
||||||
|
<Text variant="b3">{`Select who can join this ${isSpace ? 'space' : 'room'}.`}</Text>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{joinRule === 'public' && (
|
{joinRule === 'public' && (
|
||||||
<div>
|
<div>
|
||||||
<Text className="create-room__address__label" variant="b2">{isSpace ? 'Space address' : 'Room address'}</Text>
|
<Text className="create-room__address__label" variant="b2">
|
||||||
|
{isSpace ? 'Space address' : 'Room address'}
|
||||||
|
</Text>
|
||||||
<div className="create-room__address">
|
<div className="create-room__address">
|
||||||
<Text variant="b1">#</Text>
|
<Text variant="b1">#</Text>
|
||||||
<Input
|
<Input
|
||||||
value={addressValue}
|
value={addressValue}
|
||||||
onChange={validateAddress}
|
onChange={validateAddress}
|
||||||
state={(isValidAddress === false) ? 'error' : 'normal'}
|
state={isValidAddress === false ? 'error' : 'normal'}
|
||||||
forwardRef={addressRef}
|
forwardRef={addressRef}
|
||||||
placeholder="my_address"
|
placeholder="my_address"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<Text variant="b1">{`:${userHs}`}</Text>
|
<Text variant="b1">{`:${userHs}`}</Text>
|
||||||
</div>
|
</div>
|
||||||
{isValidAddress === false && <Text className="create-room__address__tip" variant="b3"><span style={{ color: 'var(--bg-danger)' }}>{`#${addressValue}:${userHs} is already in use`}</span></Text>}
|
{isValidAddress === false && (
|
||||||
|
<Text className="create-room__address__tip" variant="b3">
|
||||||
|
<span
|
||||||
|
style={{ color: 'var(--bg-danger)' }}
|
||||||
|
>{`#${addressValue}:${userHs} is already in use`}</span>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isSpace && joinRule !== 'public' && (
|
{!isSpace && joinRule !== 'public' && (
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Enable end-to-end encryption"
|
title="Enable end-to-end encryption"
|
||||||
options={<Toggle isActive={isEncrypted} onToggle={setIsEncrypted} />}
|
options={<Toggle isActive={isEncrypted} onToggle={setIsEncrypted} />}
|
||||||
content={<Text variant="b3">You 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
|
<SettingTile
|
||||||
title="Select your role"
|
title="Select your role"
|
||||||
options={(
|
options={
|
||||||
<SegmentControl
|
<SegmentControl
|
||||||
selected={roleIndex}
|
selected={roleIndex}
|
||||||
segments={[{ text: 'Admin' }, { text: 'Founder' }]}
|
segments={[{ text: 'Admin' }, { text: 'Founder' }]}
|
||||||
onSelect={setRoleIndex}
|
onSelect={setRoleIndex}
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
content={(
|
content={
|
||||||
<Text variant="b3">Selecting Admin sets 100 power level whereas Founder sets 101.</Text>
|
<Text variant="b3">Selecting Admin sets 100 power level whereas Founder sets 101.</Text>
|
||||||
)}
|
}
|
||||||
/>
|
/>
|
||||||
<Input name="topic" minHeight={174} resizable label="Topic (optional)" />
|
<Input name="topic" minHeight={174} resizable label="Topic (optional)" />
|
||||||
<div className="create-room__name-wrapper">
|
<div className="create-room__name-wrapper">
|
||||||
|
@ -231,7 +243,11 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
|
||||||
<Text>{`Creating ${isSpace ? 'space' : 'room'}...`}</Text>
|
<Text>{`Creating ${isSpace ? 'space' : 'room'}...`}</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{typeof creatingError === 'string' && <Text className="create-room__error" variant="b3">{creatingError}</Text>}
|
{typeof creatingError === 'string' && (
|
||||||
|
<Text className="create-room__error" variant="b3">
|
||||||
|
{creatingError}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -275,27 +291,22 @@ function CreateRoom() {
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
isOpen={create !== null}
|
isOpen={create !== null}
|
||||||
title={(
|
title={
|
||||||
<Text variant="s1" weight="medium" primary>
|
<Text variant="s1" weight="medium" primary>
|
||||||
{parentId ? twemojify(room.name) : 'Home'}
|
{parentId ? room.name : 'Home'}
|
||||||
<span style={{ color: 'var(--tc-surface-low)' }}>
|
<span style={{ color: 'var(--tc-surface-low)' }}>
|
||||||
{` — create ${isSpace ? 'space' : 'room'}`}
|
{` — create ${isSpace ? 'space' : 'room'}`}
|
||||||
</span>
|
</span>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
}
|
||||||
contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
|
contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
|
||||||
onRequestClose={onRequestClose}
|
onRequestClose={onRequestClose}
|
||||||
>
|
>
|
||||||
{
|
{create ? (
|
||||||
create
|
<CreateRoomContent isSpace={isSpace} parentId={parentId} onRequestClose={onRequestClose} />
|
||||||
? (
|
) : (
|
||||||
<CreateRoomContent
|
<div />
|
||||||
isSpace={isSpace}
|
)}
|
||||||
parentId={parentId}
|
|
||||||
onRequestClose={onRequestClose}
|
|
||||||
/>
|
|
||||||
) : <div />
|
|
||||||
}
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,6 @@ const EmojiGroup = React.memo(({ name, groupEmojis }) => {
|
||||||
unicode: emoji.unicode,
|
unicode: emoji.unicode,
|
||||||
shortcodes: emoji.shortcodes?.toString(),
|
shortcodes: emoji.shortcodes?.toString(),
|
||||||
hexcode: emoji.hexcode,
|
hexcode: emoji.hexcode,
|
||||||
loading: 'lazy',
|
|
||||||
}),
|
}),
|
||||||
base: TWEMOJI_BASE_URL,
|
base: TWEMOJI_BASE_URL,
|
||||||
})
|
})
|
||||||
|
@ -340,7 +339,7 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</div>
|
</div>
|
||||||
<div ref={emojiInfo} className="emoji-board__content__info">
|
<div ref={emojiInfo} className="emoji-board__content__info">
|
||||||
<div>{parse(twemoji.parse('🙂', { base: TWEMOJI_BASE_URL }))}</div>
|
<div>{parse('🙂')}</div>
|
||||||
<Text>:slight_smile:</Text>
|
<Text>:slight_smile:</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './EmojiVerification.scss';
|
import './EmojiVerification.scss';
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
|
@ -30,8 +29,9 @@ function EmojiVerificationContent({ data, requestClose }) {
|
||||||
|
|
||||||
const beginVerification = async () => {
|
const beginVerification = async () => {
|
||||||
if (
|
if (
|
||||||
isCrossVerified(mx.deviceId)
|
isCrossVerified(mx.deviceId) &&
|
||||||
&& (mx.getCrossSigningId() === null || await mx.crypto.crossSigningInfo.isStoredInKeyCache('self_signing') === false)
|
(mx.getCrossSigningId() === null ||
|
||||||
|
(await mx.crypto.crossSigningInfo.isStoredInKeyCache('self_signing')) === false)
|
||||||
) {
|
) {
|
||||||
if (!hasPrivateKey(getDefaultSSKey())) {
|
if (!hasPrivateKey(getDefaultSSKey())) {
|
||||||
const keyData = await accessSecretStorage('Emoji verification');
|
const keyData = await accessSecretStorage('Emoji verification');
|
||||||
|
@ -106,16 +106,20 @@ function EmojiVerificationContent({ data, requestClose }) {
|
||||||
{sas.sas.emoji.map((emoji, i) => (
|
{sas.sas.emoji.map((emoji, i) => (
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
<div className="emoji-verification__emoji-block" key={`${emoji[1]}-${i}`}>
|
<div className="emoji-verification__emoji-block" key={`${emoji[1]}-${i}`}>
|
||||||
<Text variant="h1">{twemojify(emoji[0])}</Text>
|
<Text variant="h1">{emoji[0]}</Text>
|
||||||
<Text>{emoji[1]}</Text>
|
<Text>{emoji[1]}</Text>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="emoji-verification__buttons">
|
<div className="emoji-verification__buttons">
|
||||||
{process ? renderWait() : (
|
{process ? (
|
||||||
|
renderWait()
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
<Button variant="primary" onClick={sasConfirm}>They match</Button>
|
<Button variant="primary" onClick={sasConfirm}>
|
||||||
<Button onClick={sasMismatch}>{'They don\'t match'}</Button>
|
They match
|
||||||
|
</Button>
|
||||||
|
<Button onClick={sasMismatch}>{"They don't match"}</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -127,9 +131,7 @@ function EmojiVerificationContent({ data, requestClose }) {
|
||||||
return (
|
return (
|
||||||
<div className="emoji-verification__content">
|
<div className="emoji-verification__content">
|
||||||
<Text>Please accept the request from other device.</Text>
|
<Text>Please accept the request from other device.</Text>
|
||||||
<div className="emoji-verification__buttons">
|
<div className="emoji-verification__buttons">{renderWait()}</div>
|
||||||
{renderWait()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -138,11 +140,13 @@ function EmojiVerificationContent({ data, requestClose }) {
|
||||||
<div className="emoji-verification__content">
|
<div className="emoji-verification__content">
|
||||||
<Text>Click accept to start the verification process.</Text>
|
<Text>Click accept to start the verification process.</Text>
|
||||||
<div className="emoji-verification__buttons">
|
<div className="emoji-verification__buttons">
|
||||||
{
|
{process ? (
|
||||||
process
|
renderWait()
|
||||||
? renderWait()
|
) : (
|
||||||
: <Button variant="primary" onClick={beginVerification}>Accept</Button>
|
<Button variant="primary" onClick={beginVerification}>
|
||||||
}
|
Accept
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -180,19 +184,19 @@ function EmojiVerification() {
|
||||||
<Dialog
|
<Dialog
|
||||||
isOpen={data !== null}
|
isOpen={data !== null}
|
||||||
className="emoji-verification"
|
className="emoji-verification"
|
||||||
title={(
|
title={
|
||||||
<Text variant="s1" weight="medium" primary>
|
<Text variant="s1" weight="medium" primary>
|
||||||
Emoji verification
|
Emoji verification
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
}
|
||||||
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
|
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
|
||||||
onRequestClose={requestClose}
|
onRequestClose={requestClose}
|
||||||
>
|
>
|
||||||
{
|
{data !== null ? (
|
||||||
data !== null
|
<EmojiVerificationContent data={data} requestClose={requestClose} />
|
||||||
? <EmojiVerificationContent data={data} requestClose={requestClose} />
|
) : (
|
||||||
: <div />
|
<div />
|
||||||
}
|
)}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,6 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './DrawerBreadcrumb.scss';
|
import './DrawerBreadcrumb.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import { selectTab, selectSpace } from '../../../client/action/navigation';
|
import { selectTab, selectSpace } from '../../../client/action/navigation';
|
||||||
|
@ -49,8 +47,9 @@ function DrawerBreadcrumb({ spaceId }) {
|
||||||
}, [spaceId]);
|
}, [spaceId]);
|
||||||
|
|
||||||
function getHomeNotiExcept(childId) {
|
function getHomeNotiExcept(childId) {
|
||||||
const orphans = roomList.getOrphans()
|
const orphans = roomList
|
||||||
.filter((id) => (id !== childId))
|
.getOrphans()
|
||||||
|
.filter((id) => id !== childId)
|
||||||
.filter((id) => !accountData.spaceShortcut.has(id));
|
.filter((id) => !accountData.spaceShortcut.has(id));
|
||||||
|
|
||||||
let noti = null;
|
let noti = null;
|
||||||
|
@ -94,36 +93,35 @@ function DrawerBreadcrumb({ spaceId }) {
|
||||||
<div className="drawer-breadcrumb__wrapper">
|
<div className="drawer-breadcrumb__wrapper">
|
||||||
<ScrollView ref={scrollRef} horizontal vertical={false} invisible>
|
<ScrollView ref={scrollRef} horizontal vertical={false} invisible>
|
||||||
<div className="drawer-breadcrumb">
|
<div className="drawer-breadcrumb">
|
||||||
{
|
{spacePath.map((id, index) => {
|
||||||
spacePath.map((id, index) => {
|
const noti =
|
||||||
const noti = (id !== cons.tabs.HOME && index < spacePath.length)
|
id !== cons.tabs.HOME && index < spacePath.length
|
||||||
? getNotiExcept(id, (index === spacePath.length - 1) ? null : spacePath[index + 1])
|
? getNotiExcept(id, index === spacePath.length - 1 ? null : spacePath[index + 1])
|
||||||
: getHomeNotiExcept((index === spacePath.length - 1) ? null : spacePath[index + 1]);
|
: getHomeNotiExcept(index === spacePath.length - 1 ? null : spacePath[index + 1]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment
|
<React.Fragment key={id}>
|
||||||
key={id}
|
{index !== 0 && <RawIcon size="extra-small" src={ChevronRightIC} />}
|
||||||
|
<Button
|
||||||
|
className={
|
||||||
|
index === spacePath.length - 1 ? 'drawer-breadcrumb__btn--selected' : ''
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
if (id === cons.tabs.HOME) selectTab(id);
|
||||||
|
else selectSpace(id);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{ index !== 0 && <RawIcon size="extra-small" src={ChevronRightIC} />}
|
<Text variant="b2">{id === cons.tabs.HOME ? 'Home' : mx.getRoom(id).name}</Text>
|
||||||
<Button
|
{noti !== null && (
|
||||||
className={index === spacePath.length - 1 ? 'drawer-breadcrumb__btn--selected' : ''}
|
<NotificationBadge
|
||||||
onClick={() => {
|
alert={noti.highlight !== 0}
|
||||||
if (id === cons.tabs.HOME) selectTab(id);
|
content={noti.total > 0 ? abbreviateNumber(noti.total) : null}
|
||||||
else selectSpace(id);
|
/>
|
||||||
}}
|
)}
|
||||||
>
|
</Button>
|
||||||
<Text variant="b2">{id === cons.tabs.HOME ? 'Home' : twemojify(mx.getRoom(id).name)}</Text>
|
</React.Fragment>
|
||||||
{ noti !== null && (
|
);
|
||||||
<NotificationBadge
|
})}
|
||||||
alert={noti.highlight !== 0}
|
|
||||||
content={noti.total > 0 ? abbreviateNumber(noti.total) : null}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
<div style={{ width: 'var(--sp-extra-tight)', height: '100%' }} />
|
<div style={{ width: 'var(--sp-extra-tight)', height: '100%' }} />
|
||||||
</div>
|
</div>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
|
@ -2,13 +2,16 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './DrawerHeader.scss';
|
import './DrawerHeader.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import {
|
import {
|
||||||
openPublicRooms, openCreateRoom, openSpaceManage, openJoinAlias,
|
openPublicRooms,
|
||||||
openSpaceAddExisting, openInviteUser, openReusableContextMenu,
|
openCreateRoom,
|
||||||
|
openSpaceManage,
|
||||||
|
openJoinAlias,
|
||||||
|
openSpaceAddExisting,
|
||||||
|
openInviteUser,
|
||||||
|
openReusableContextMenu,
|
||||||
} from '../../../client/action/navigation';
|
} from '../../../client/action/navigation';
|
||||||
import { getEventCords } from '../../../util/common';
|
import { getEventCords } from '../../../util/common';
|
||||||
|
|
||||||
|
@ -40,46 +43,64 @@ export function HomeSpaceOptions({ spaceId, afterOptionSelect }) {
|
||||||
<MenuHeader>Add rooms or spaces</MenuHeader>
|
<MenuHeader>Add rooms or spaces</MenuHeader>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
iconSrc={SpacePlusIC}
|
iconSrc={SpacePlusIC}
|
||||||
onClick={() => { afterOptionSelect(); openCreateRoom(true, spaceId); }}
|
onClick={() => {
|
||||||
|
afterOptionSelect();
|
||||||
|
openCreateRoom(true, spaceId);
|
||||||
|
}}
|
||||||
disabled={!canManage}
|
disabled={!canManage}
|
||||||
>
|
>
|
||||||
Create new space
|
Create new space
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
iconSrc={HashPlusIC}
|
iconSrc={HashPlusIC}
|
||||||
onClick={() => { afterOptionSelect(); openCreateRoom(false, spaceId); }}
|
onClick={() => {
|
||||||
|
afterOptionSelect();
|
||||||
|
openCreateRoom(false, spaceId);
|
||||||
|
}}
|
||||||
disabled={!canManage}
|
disabled={!canManage}
|
||||||
>
|
>
|
||||||
Create new room
|
Create new room
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{ !spaceId && (
|
{!spaceId && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
iconSrc={HashGlobeIC}
|
iconSrc={HashGlobeIC}
|
||||||
onClick={() => { afterOptionSelect(); openPublicRooms(); }}
|
onClick={() => {
|
||||||
|
afterOptionSelect();
|
||||||
|
openPublicRooms();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Explore public rooms
|
Explore public rooms
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{ !spaceId && (
|
{!spaceId && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
iconSrc={PlusIC}
|
iconSrc={PlusIC}
|
||||||
onClick={() => { afterOptionSelect(); openJoinAlias(); }}
|
onClick={() => {
|
||||||
|
afterOptionSelect();
|
||||||
|
openJoinAlias();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Join with address
|
Join with address
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{ spaceId && (
|
{spaceId && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
iconSrc={PlusIC}
|
iconSrc={PlusIC}
|
||||||
onClick={() => { afterOptionSelect(); openSpaceAddExisting(spaceId); }}
|
onClick={() => {
|
||||||
|
afterOptionSelect();
|
||||||
|
openSpaceAddExisting(spaceId);
|
||||||
|
}}
|
||||||
disabled={!canManage}
|
disabled={!canManage}
|
||||||
>
|
>
|
||||||
Add existing
|
Add existing
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{ spaceId && (
|
{spaceId && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => { afterOptionSelect(); openSpaceManage(spaceId); }}
|
onClick={() => {
|
||||||
|
afterOptionSelect();
|
||||||
|
openSpaceManage(spaceId);
|
||||||
|
}}
|
||||||
iconSrc={HashSearchIC}
|
iconSrc={HashSearchIC}
|
||||||
>
|
>
|
||||||
Manage rooms
|
Manage rooms
|
||||||
|
@ -102,24 +123,20 @@ function DrawerHeader({ selectedTab, spaceId }) {
|
||||||
|
|
||||||
const isDMTab = selectedTab === cons.tabs.DIRECTS;
|
const isDMTab = selectedTab === cons.tabs.DIRECTS;
|
||||||
const room = mx.getRoom(spaceId);
|
const room = mx.getRoom(spaceId);
|
||||||
const spaceName = isDMTab ? null : (room?.name || null);
|
const spaceName = isDMTab ? null : room?.name || null;
|
||||||
|
|
||||||
const openSpaceOptions = (e) => {
|
const openSpaceOptions = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
openReusableContextMenu(
|
openReusableContextMenu('bottom', getEventCords(e, '.header'), (closeMenu) => (
|
||||||
'bottom',
|
<SpaceOptions roomId={spaceId} afterOptionSelect={closeMenu} />
|
||||||
getEventCords(e, '.header'),
|
));
|
||||||
(closeMenu) => <SpaceOptions roomId={spaceId} afterOptionSelect={closeMenu} />,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const openHomeSpaceOptions = (e) => {
|
const openHomeSpaceOptions = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
openReusableContextMenu(
|
openReusableContextMenu('right', getEventCords(e, '.ic-btn'), (closeMenu) => (
|
||||||
'right',
|
<HomeSpaceOptions spaceId={spaceId} afterOptionSelect={closeMenu} />
|
||||||
getEventCords(e, '.ic-btn'),
|
));
|
||||||
(closeMenu) => <HomeSpaceOptions spaceId={spaceId} afterOptionSelect={closeMenu} />,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -132,18 +149,31 @@ function DrawerHeader({ selectedTab, spaceId }) {
|
||||||
onMouseUp={(e) => blurOnBubbling(e, '.drawer-header__btn')}
|
onMouseUp={(e) => blurOnBubbling(e, '.drawer-header__btn')}
|
||||||
>
|
>
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
<Text variant="s1" weight="medium" primary>{twemojify(spaceName)}</Text>
|
<Text variant="s1" weight="medium" primary>
|
||||||
|
{spaceName}
|
||||||
|
</Text>
|
||||||
</TitleWrapper>
|
</TitleWrapper>
|
||||||
<RawIcon size="small" src={ChevronBottomIC} />
|
<RawIcon size="small" src={ChevronBottomIC} />
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
<Text variant="s1" weight="medium" primary>{tabName}</Text>
|
<Text variant="s1" weight="medium" primary>
|
||||||
|
{tabName}
|
||||||
|
</Text>
|
||||||
</TitleWrapper>
|
</TitleWrapper>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{ isDMTab && <IconButton onClick={() => openInviteUser()} tooltip="Start DM" src={PlusIC} size="small" /> }
|
{isDMTab && (
|
||||||
{ !isDMTab && <IconButton onClick={openHomeSpaceOptions} tooltip="Add rooms/spaces" src={PlusIC} size="small" /> }
|
<IconButton onClick={() => openInviteUser()} tooltip="Start DM" src={PlusIC} size="small" />
|
||||||
|
)}
|
||||||
|
{!isDMTab && (
|
||||||
|
<IconButton
|
||||||
|
onClick={openHomeSpaceOptions}
|
||||||
|
tooltip="Add rooms/spaces"
|
||||||
|
src={PlusIC}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Header>
|
</Header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
|
@ -22,7 +21,9 @@ function ProfileEditor({ userId }) {
|
||||||
const user = mx.getUser(mx.getUserId());
|
const user = mx.getUser(mx.getUserId());
|
||||||
|
|
||||||
const displayNameRef = useRef(null);
|
const displayNameRef = useRef(null);
|
||||||
const [avatarSrc, setAvatarSrc] = useState(user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null);
|
const [avatarSrc, setAvatarSrc] = useState(
|
||||||
|
user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null
|
||||||
|
);
|
||||||
const [username, setUsername] = useState(user.displayName);
|
const [username, setUsername] = useState(user.displayName);
|
||||||
const [disabled, setDisabled] = useState(true);
|
const [disabled, setDisabled] = useState(true);
|
||||||
|
|
||||||
|
@ -44,7 +45,7 @@ function ProfileEditor({ userId }) {
|
||||||
'Remove avatar',
|
'Remove avatar',
|
||||||
'Are you sure that you want to remove avatar?',
|
'Are you sure that you want to remove avatar?',
|
||||||
'Remove',
|
'Remove',
|
||||||
'caution',
|
'caution'
|
||||||
);
|
);
|
||||||
if (isConfirmed) {
|
if (isConfirmed) {
|
||||||
mx.setAvatarUrl('');
|
mx.setAvatarUrl('');
|
||||||
|
@ -79,7 +80,10 @@ function ProfileEditor({ userId }) {
|
||||||
<form
|
<form
|
||||||
className="profile-editor__form"
|
className="profile-editor__form"
|
||||||
style={{ marginBottom: avatarSrc ? '24px' : '0' }}
|
style={{ marginBottom: avatarSrc ? '24px' : '0' }}
|
||||||
onSubmit={(e) => { e.preventDefault(); saveDisplayName(); }}
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
saveDisplayName();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
label={`Display name of ${mx.getUserId()}`}
|
label={`Display name of ${mx.getUserId()}`}
|
||||||
|
@ -87,7 +91,9 @@ function ProfileEditor({ userId }) {
|
||||||
value={mx.getUser(mx.getUserId()).displayName}
|
value={mx.getUser(mx.getUserId()).displayName}
|
||||||
forwardRef={displayNameRef}
|
forwardRef={displayNameRef}
|
||||||
/>
|
/>
|
||||||
<Button variant="primary" type="submit" disabled={disabled}>Save</Button>
|
<Button variant="primary" type="submit" disabled={disabled}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
<Button onClick={cancelDisplayNameChanges}>Cancel</Button>
|
<Button onClick={cancelDisplayNameChanges}>Cancel</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -95,7 +101,9 @@ function ProfileEditor({ userId }) {
|
||||||
const renderInfo = () => (
|
const renderInfo = () => (
|
||||||
<div className="profile-editor__info" style={{ marginBottom: avatarSrc ? '24px' : '0' }}>
|
<div className="profile-editor__info" style={{ marginBottom: avatarSrc ? '24px' : '0' }}>
|
||||||
<div>
|
<div>
|
||||||
<Text variant="h2" primary weight="medium">{twemojify(username) ?? userId}</Text>
|
<Text variant="h2" primary weight="medium">
|
||||||
|
{username ?? userId}
|
||||||
|
</Text>
|
||||||
<IconButton
|
<IconButton
|
||||||
src={PencilIC}
|
src={PencilIC}
|
||||||
size="extra-small"
|
size="extra-small"
|
||||||
|
@ -116,9 +124,7 @@ function ProfileEditor({ userId }) {
|
||||||
onUpload={handleAvatarUpload}
|
onUpload={handleAvatarUpload}
|
||||||
onRequestRemove={() => handleAvatarUpload(null)}
|
onRequestRemove={() => handleAvatarUpload(null)}
|
||||||
/>
|
/>
|
||||||
{
|
{isEditing ? renderForm() : renderInfo()}
|
||||||
isEditing ? renderForm() : renderInfo()
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,6 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './ProfileViewer.scss';
|
import './ProfileViewer.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
|
@ -11,7 +9,11 @@ import { selectRoom, openReusableContextMenu } from '../../../client/action/navi
|
||||||
import * as roomActions from '../../../client/action/room';
|
import * as roomActions from '../../../client/action/room';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getUsername, getUsernameOfRoomMember, getPowerLabel, hasDMWith, hasDevices,
|
getUsername,
|
||||||
|
getUsernameOfRoomMember,
|
||||||
|
getPowerLabel,
|
||||||
|
hasDMWith,
|
||||||
|
hasDevices,
|
||||||
} from '../../../util/matrixUtil';
|
} from '../../../util/matrixUtil';
|
||||||
import { getEventCords } from '../../../util/common';
|
import { getEventCords } from '../../../util/common';
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
|
@ -34,25 +36,21 @@ import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||||
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
||||||
|
|
||||||
function ModerationTools({
|
function ModerationTools({ roomId, userId }) {
|
||||||
roomId, userId,
|
|
||||||
}) {
|
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
const roomMember = room.getMember(userId);
|
const roomMember = room.getMember(userId);
|
||||||
|
|
||||||
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
||||||
const powerLevel = roomMember?.powerLevel || 0;
|
const powerLevel = roomMember?.powerLevel || 0;
|
||||||
const canIKick = (
|
const canIKick =
|
||||||
roomMember?.membership === 'join'
|
roomMember?.membership === 'join' &&
|
||||||
&& room.currentState.hasSufficientPowerLevelFor('kick', myPowerLevel)
|
room.currentState.hasSufficientPowerLevelFor('kick', myPowerLevel) &&
|
||||||
&& powerLevel < myPowerLevel
|
powerLevel < myPowerLevel;
|
||||||
);
|
const canIBan =
|
||||||
const canIBan = (
|
['join', 'leave'].includes(roomMember?.membership) &&
|
||||||
['join', 'leave'].includes(roomMember?.membership)
|
room.currentState.hasSufficientPowerLevelFor('ban', myPowerLevel) &&
|
||||||
&& room.currentState.hasSufficientPowerLevelFor('ban', myPowerLevel)
|
powerLevel < myPowerLevel;
|
||||||
&& powerLevel < myPowerLevel
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleKick = (e) => {
|
const handleKick = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -120,13 +118,14 @@ function SessionInfo({ userId }) {
|
||||||
<div className="session-info__chips">
|
<div className="session-info__chips">
|
||||||
{devices === null && <Text variant="b2">Loading sessions...</Text>}
|
{devices === null && <Text variant="b2">Loading sessions...</Text>}
|
||||||
{devices?.length === 0 && <Text variant="b2">No session found.</Text>}
|
{devices?.length === 0 && <Text variant="b2">No session found.</Text>}
|
||||||
{devices !== null && (devices.map((device) => (
|
{devices !== null &&
|
||||||
<Chip
|
devices.map((device) => (
|
||||||
key={device.deviceId}
|
<Chip
|
||||||
iconSrc={ShieldEmptyIC}
|
key={device.deviceId}
|
||||||
text={device.getDisplayName() || device.deviceId}
|
iconSrc={ShieldEmptyIC}
|
||||||
/>
|
text={device.getDisplayName() || device.deviceId}
|
||||||
)))}
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -137,7 +136,9 @@ function SessionInfo({ userId }) {
|
||||||
onClick={() => setIsVisible(!isVisible)}
|
onClick={() => setIsVisible(!isVisible)}
|
||||||
iconSrc={isVisible ? ChevronBottomIC : ChevronRightIC}
|
iconSrc={isVisible ? ChevronBottomIC : ChevronRightIC}
|
||||||
>
|
>
|
||||||
<Text variant="b2">{`View ${devices?.length > 0 ? `${devices.length} ` : ''}sessions`}</Text>
|
<Text variant="b2">{`View ${
|
||||||
|
devices?.length > 0 ? `${devices.length} ` : ''
|
||||||
|
}sessions`}</Text>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{renderSessionChips()}
|
{renderSessionChips()}
|
||||||
</div>
|
</div>
|
||||||
|
@ -164,7 +165,8 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
|
||||||
|
|
||||||
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
||||||
const userPL = room.getMember(userId)?.powerLevel || 0;
|
const userPL = room.getMember(userId)?.powerLevel || 0;
|
||||||
const canIKick = room.currentState.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel;
|
const canIKick =
|
||||||
|
room.currentState.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel;
|
||||||
|
|
||||||
const isBanned = member?.membership === 'ban';
|
const isBanned = member?.membership === 'ban';
|
||||||
|
|
||||||
|
@ -246,31 +248,19 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="profile-viewer__buttons">
|
<div className="profile-viewer__buttons">
|
||||||
<Button
|
<Button variant="primary" onClick={openDM} disabled={isCreatingDM}>
|
||||||
variant="primary"
|
|
||||||
onClick={openDM}
|
|
||||||
disabled={isCreatingDM}
|
|
||||||
>
|
|
||||||
{isCreatingDM ? 'Creating room...' : 'Message'}
|
{isCreatingDM ? 'Creating room...' : 'Message'}
|
||||||
</Button>
|
</Button>
|
||||||
{ isBanned && canIKick && (
|
{isBanned && canIKick && (
|
||||||
<Button
|
<Button variant="positive" onClick={() => roomActions.unban(roomId, userId)}>
|
||||||
variant="positive"
|
|
||||||
onClick={() => roomActions.unban(roomId, userId)}
|
|
||||||
>
|
|
||||||
Unban
|
Unban
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{ (isInvited ? canIKick : room.canInvite(mx.getUserId())) && isInvitable && (
|
{(isInvited ? canIKick : room.canInvite(mx.getUserId())) && isInvitable && (
|
||||||
<Button
|
<Button onClick={toggleInvite} disabled={isInviting}>
|
||||||
onClick={toggleInvite}
|
{isInvited
|
||||||
disabled={isInviting}
|
? `${isInviting ? 'Disinviting...' : 'Disinvite'}`
|
||||||
>
|
: `${isInviting ? 'Inviting...' : 'Invite'}`}
|
||||||
{
|
|
||||||
isInvited
|
|
||||||
? `${isInviting ? 'Disinviting...' : 'Disinvite'}`
|
|
||||||
: `${isInviting ? 'Inviting...' : 'Invite'}`
|
|
||||||
}
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
|
@ -278,11 +268,9 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
|
||||||
onClick={toggleIgnore}
|
onClick={toggleIgnore}
|
||||||
disabled={isIgnoring}
|
disabled={isIgnoring}
|
||||||
>
|
>
|
||||||
{
|
{isUserIgnored
|
||||||
isUserIgnored
|
? `${isIgnoring ? 'Unignoring...' : 'Unignore'}`
|
||||||
? `${isIgnoring ? 'Unignoring...' : 'Unignore'}`
|
: `${isIgnoring ? 'Ignoring...' : 'Ignore'}`}
|
||||||
: `${isIgnoring ? 'Ignoring...' : 'Ignore'}`
|
|
||||||
}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -326,8 +314,8 @@ function useRerenderOnProfileChange(roomId, userId) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleProfileChange = (mEvent, member) => {
|
const handleProfileChange = (mEvent, member) => {
|
||||||
if (
|
if (
|
||||||
mEvent.getRoomId() === roomId
|
mEvent.getRoomId() === roomId &&
|
||||||
&& (member.userId === userId || member.userId === mx.getUserId())
|
(member.userId === userId || member.userId === mx.getUserId())
|
||||||
) {
|
) {
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
}
|
}
|
||||||
|
@ -352,20 +340,22 @@ function ProfileViewer() {
|
||||||
const roomMember = room.getMember(userId);
|
const roomMember = room.getMember(userId);
|
||||||
const username = roomMember ? getUsernameOfRoomMember(roomMember) : getUsername(userId);
|
const username = roomMember ? getUsernameOfRoomMember(roomMember) : getUsername(userId);
|
||||||
const avatarMxc = roomMember?.getMxcAvatarUrl?.() || mx.getUser(userId)?.avatarUrl;
|
const avatarMxc = roomMember?.getMxcAvatarUrl?.() || mx.getUser(userId)?.avatarUrl;
|
||||||
const avatarUrl = (avatarMxc && avatarMxc !== 'null') ? mx.mxcUrlToHttp(avatarMxc, 80, 80, 'crop') : null;
|
const avatarUrl =
|
||||||
|
avatarMxc && avatarMxc !== 'null' ? mx.mxcUrlToHttp(avatarMxc, 80, 80, 'crop') : null;
|
||||||
|
|
||||||
const powerLevel = roomMember?.powerLevel || 0;
|
const powerLevel = roomMember?.powerLevel || 0;
|
||||||
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
||||||
|
|
||||||
const canChangeRole = (
|
const canChangeRole =
|
||||||
room.currentState.maySendEvent('m.room.power_levels', mx.getUserId())
|
room.currentState.maySendEvent('m.room.power_levels', mx.getUserId()) &&
|
||||||
&& (powerLevel < myPowerLevel || userId === mx.getUserId())
|
(powerLevel < myPowerLevel || userId === mx.getUserId());
|
||||||
);
|
|
||||||
|
|
||||||
const handleChangePowerLevel = async (newPowerLevel) => {
|
const handleChangePowerLevel = async (newPowerLevel) => {
|
||||||
if (newPowerLevel === powerLevel) return;
|
if (newPowerLevel === powerLevel) return;
|
||||||
const SHARED_POWER_MSG = 'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?';
|
const SHARED_POWER_MSG =
|
||||||
const DEMOTING_MYSELF_MSG = 'You will not be able to undo this change as you are demoting yourself. Are you sure?';
|
'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?';
|
||||||
|
const DEMOTING_MYSELF_MSG =
|
||||||
|
'You will not be able to undo this change as you are demoting yourself. Are you sure?';
|
||||||
|
|
||||||
const isSharedPower = newPowerLevel === myPowerLevel;
|
const isSharedPower = newPowerLevel === myPowerLevel;
|
||||||
const isDemotingMyself = userId === mx.getUserId();
|
const isDemotingMyself = userId === mx.getUserId();
|
||||||
|
@ -374,7 +364,7 @@ function ProfileViewer() {
|
||||||
'Change power level',
|
'Change power level',
|
||||||
isSharedPower ? SHARED_POWER_MSG : DEMOTING_MYSELF_MSG,
|
isSharedPower ? SHARED_POWER_MSG : DEMOTING_MYSELF_MSG,
|
||||||
'Change',
|
'Change',
|
||||||
'caution',
|
'caution'
|
||||||
);
|
);
|
||||||
if (!isConfirmed) return;
|
if (!isConfirmed) return;
|
||||||
roomActions.setPowerLevel(roomId, userId, newPowerLevel);
|
roomActions.setPowerLevel(roomId, userId, newPowerLevel);
|
||||||
|
@ -384,20 +374,16 @@ function ProfileViewer() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePowerSelector = (e) => {
|
const handlePowerSelector = (e) => {
|
||||||
openReusableContextMenu(
|
openReusableContextMenu('bottom', getEventCords(e, '.btn-surface'), (closeMenu) => (
|
||||||
'bottom',
|
<PowerLevelSelector
|
||||||
getEventCords(e, '.btn-surface'),
|
value={powerLevel}
|
||||||
(closeMenu) => (
|
max={myPowerLevel}
|
||||||
<PowerLevelSelector
|
onSelect={(pl) => {
|
||||||
value={powerLevel}
|
closeMenu();
|
||||||
max={myPowerLevel}
|
handleChangePowerLevel(pl);
|
||||||
onSelect={(pl) => {
|
}}
|
||||||
closeMenu();
|
/>
|
||||||
handleChangePowerLevel(pl);
|
));
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -405,8 +391,10 @@ function ProfileViewer() {
|
||||||
<div className="profile-viewer__user">
|
<div className="profile-viewer__user">
|
||||||
<Avatar imageSrc={avatarUrl} text={username} bgColor={colorMXID(userId)} size="large" />
|
<Avatar imageSrc={avatarUrl} text={username} bgColor={colorMXID(userId)} size="large" />
|
||||||
<div className="profile-viewer__user__info">
|
<div className="profile-viewer__user__info">
|
||||||
<Text variant="s1" weight="medium">{twemojify(username)}</Text>
|
<Text variant="s1" weight="medium">
|
||||||
<Text variant="b2">{twemojify(userId)}</Text>
|
{username}
|
||||||
|
</Text>
|
||||||
|
<Text variant="b2">{userId}</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="profile-viewer__user__role">
|
<div className="profile-viewer__user__role">
|
||||||
<Text variant="b3">Role</Text>
|
<Text variant="b3">Role</Text>
|
||||||
|
@ -420,7 +408,7 @@ function ProfileViewer() {
|
||||||
</div>
|
</div>
|
||||||
<ModerationTools roomId={roomId} userId={userId} />
|
<ModerationTools roomId={roomId} userId={userId} />
|
||||||
<SessionInfo userId={userId} />
|
<SessionInfo userId={userId} />
|
||||||
{ userId !== mx.getUserId() && (
|
{userId !== mx.getUserId() && (
|
||||||
<ProfileFooter roomId={roomId} userId={userId} onRequestClose={closeDialog} />
|
<ProfileFooter roomId={roomId} userId={userId} onRequestClose={closeDialog} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,9 +3,6 @@ import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './RoomViewCmdBar.scss';
|
import './RoomViewCmdBar.scss';
|
||||||
import parse from 'html-react-parser';
|
import parse from 'html-react-parser';
|
||||||
import twemoji from 'twemoji';
|
|
||||||
|
|
||||||
import { twemojify, TWEMOJI_BASE_URL } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import { getEmojiForCompletion } from '../emoji-board/custom-emoji';
|
import { getEmojiForCompletion } from '../emoji-board/custom-emoji';
|
||||||
|
@ -51,19 +48,6 @@ function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
|
||||||
function renderEmojiSuggestion(emPrefix, emos) {
|
function renderEmojiSuggestion(emPrefix, emos) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
|
|
||||||
// Renders a small Twemoji
|
|
||||||
function renderTwemoji(emoji) {
|
|
||||||
return parse(
|
|
||||||
twemoji.parse(emoji.unicode, {
|
|
||||||
attributes: () => ({
|
|
||||||
unicode: emoji.unicode,
|
|
||||||
shortcodes: emoji.shortcodes?.toString(),
|
|
||||||
}),
|
|
||||||
base: TWEMOJI_BASE_URL,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render a custom emoji
|
// Render a custom emoji
|
||||||
function renderCustomEmoji(emoji) {
|
function renderCustomEmoji(emoji) {
|
||||||
return (
|
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) {
|
function renderEmoji(emoji) {
|
||||||
if (emoji.mxc) {
|
if (emoji.mxc) {
|
||||||
return renderCustomEmoji(emoji);
|
return renderCustomEmoji(emoji);
|
||||||
}
|
}
|
||||||
return renderTwemoji(emoji);
|
return parse(emoji.unicode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return emos.map((emoji) => (
|
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>
|
</CmdItem>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
/* eslint-disable react/prop-types */
|
/* eslint-disable react/prop-types */
|
||||||
import React, {
|
import React, { useState, useEffect, useLayoutEffect, useCallback, useRef } from 'react';
|
||||||
useState, useEffect, useLayoutEffect, useCallback, useRef,
|
|
||||||
} from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './RoomViewContent.scss';
|
import './RoomViewContent.scss';
|
||||||
|
|
||||||
import dateFormat from 'dateformat';
|
import dateFormat from 'dateformat';
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
|
@ -44,11 +41,7 @@ function loadingMsgPlaceholders(key, count = 2) {
|
||||||
return pl;
|
return pl;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return <React.Fragment key={`placeholder-container${key}`}>{genPlaceholders()}</React.Fragment>;
|
||||||
<React.Fragment key={`placeholder-container${key}`}>
|
|
||||||
{genPlaceholders()}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function RoomIntroContainer({ event, timeline }) {
|
function RoomIntroContainer({ event, timeline }) {
|
||||||
|
@ -59,28 +52,27 @@ function RoomIntroContainer({ event, timeline }) {
|
||||||
const roomTopic = room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
|
const roomTopic = room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
|
||||||
const isDM = roomList.directs.has(timeline.roomId);
|
const isDM = roomList.directs.has(timeline.roomId);
|
||||||
let avatarSrc = room.getAvatarUrl(mx.baseUrl, 80, 80, 'crop');
|
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 heading = isDM ? room.name : `Welcome to ${room.name}`;
|
||||||
const topic = twemojify(roomTopic || '', undefined, true);
|
const topic = roomTopic || '';
|
||||||
const nameJsx = twemojify(room.name);
|
const nameJsx = room.name;
|
||||||
const desc = isDM
|
const desc = isDM ? (
|
||||||
? (
|
<>
|
||||||
<>
|
This is the beginning of your direct message history with @<b>{nameJsx}</b>
|
||||||
This is the beginning of your direct message history with @
|
{'. '}
|
||||||
<b>{nameJsx}</b>
|
{topic}
|
||||||
{'. '}
|
</>
|
||||||
{topic}
|
) : (
|
||||||
</>
|
<>
|
||||||
)
|
{'This is the beginning of the '}
|
||||||
: (
|
<b>{nameJsx}</b>
|
||||||
<>
|
{' room. '}
|
||||||
{'This is the beginning of the '}
|
{topic}
|
||||||
<b>{nameJsx}</b>
|
</>
|
||||||
{' room. '}
|
);
|
||||||
{topic}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleUpdate = () => nameForceUpdate();
|
const handleUpdate = () => nameForceUpdate();
|
||||||
|
@ -96,7 +88,7 @@ function RoomIntroContainer({ event, timeline }) {
|
||||||
roomId={timeline.roomId}
|
roomId={timeline.roomId}
|
||||||
avatarSrc={avatarSrc}
|
avatarSrc={avatarSrc}
|
||||||
name={room.name}
|
name={room.name}
|
||||||
heading={twemojify(heading)}
|
heading={heading}
|
||||||
desc={desc}
|
desc={desc}
|
||||||
time={event ? `Created at ${dateFormat(event.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null}
|
time={event ? `Created at ${dateFormat(event.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null}
|
||||||
/>
|
/>
|
||||||
|
@ -118,21 +110,13 @@ function handleOnClickCapture(e) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEvent(
|
function renderEvent(roomTimeline, mEvent, prevMEvent, isFocus, isEdit, setEdit, cancelEdit) {
|
||||||
roomTimeline,
|
const isBodyOnly =
|
||||||
mEvent,
|
prevMEvent !== null &&
|
||||||
prevMEvent,
|
prevMEvent.getSender() === mEvent.getSender() &&
|
||||||
isFocus,
|
prevMEvent.getType() !== 'm.room.member' &&
|
||||||
isEdit,
|
prevMEvent.getType() !== 'm.room.create' &&
|
||||||
setEdit,
|
diffMinutes(mEvent.getDate(), prevMEvent.getDate()) <= MAX_MSG_DIFF_MINUTES;
|
||||||
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();
|
const timestamp = mEvent.getTs();
|
||||||
|
|
||||||
if (mEvent.getType() === 'm.room.member') {
|
if (mEvent.getType() === 'm.room.member') {
|
||||||
|
@ -221,7 +205,7 @@ function usePaginate(
|
||||||
readUptoEvtStore,
|
readUptoEvtStore,
|
||||||
forceUpdateLimit,
|
forceUpdateLimit,
|
||||||
timelineScrollRef,
|
timelineScrollRef,
|
||||||
eventLimitRef,
|
eventLimitRef
|
||||||
) {
|
) {
|
||||||
const [info, setInfo] = useState(null);
|
const [info, setInfo] = useState(null);
|
||||||
|
|
||||||
|
@ -234,10 +218,12 @@ function usePaginate(
|
||||||
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
|
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
|
||||||
}
|
}
|
||||||
limit.paginate(backwards, PAG_LIMIT, roomTimeline.timeline.length);
|
limit.paginate(backwards, PAG_LIMIT, roomTimeline.timeline.length);
|
||||||
setTimeout(() => setInfo({
|
setTimeout(() =>
|
||||||
backwards,
|
setInfo({
|
||||||
loaded,
|
backwards,
|
||||||
}));
|
loaded,
|
||||||
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
|
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -283,17 +269,17 @@ function useHandleScroll(
|
||||||
readUptoEvtStore,
|
readUptoEvtStore,
|
||||||
forceUpdateLimit,
|
forceUpdateLimit,
|
||||||
timelineScrollRef,
|
timelineScrollRef,
|
||||||
eventLimitRef,
|
eventLimitRef
|
||||||
) {
|
) {
|
||||||
const handleScroll = useCallback(() => {
|
const handleScroll = useCallback(() => {
|
||||||
const timelineScroll = timelineScrollRef.current;
|
const timelineScroll = timelineScrollRef.current;
|
||||||
const limit = eventLimitRef.current;
|
const limit = eventLimitRef.current;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
// emit event to toggle scrollToBottom button visibility
|
// emit event to toggle scrollToBottom button visibility
|
||||||
const isAtBottom = (
|
const isAtBottom =
|
||||||
timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()
|
timelineScroll.bottom < 16 &&
|
||||||
&& limit.length >= roomTimeline.timeline.length
|
!roomTimeline.canPaginateForward() &&
|
||||||
);
|
limit.length >= roomTimeline.timeline.length;
|
||||||
roomTimeline.emit(cons.events.roomTimeline.AT_BOTTOM, isAtBottom);
|
roomTimeline.emit(cons.events.roomTimeline.AT_BOTTOM, isAtBottom);
|
||||||
if (isAtBottom && readUptoEvtStore.getItem()) {
|
if (isAtBottom && readUptoEvtStore.getItem()) {
|
||||||
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
|
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
|
||||||
|
@ -363,7 +349,8 @@ function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, event
|
||||||
setEvent(event);
|
setEvent(event);
|
||||||
return;
|
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) {
|
if (isRelates) {
|
||||||
setEvent(event);
|
setEvent(event);
|
||||||
return;
|
return;
|
||||||
|
@ -409,7 +396,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
||||||
readUptoEvtStore,
|
readUptoEvtStore,
|
||||||
forceUpdateLimit,
|
forceUpdateLimit,
|
||||||
timelineScrollRef,
|
timelineScrollRef,
|
||||||
eventLimitRef,
|
eventLimitRef
|
||||||
);
|
);
|
||||||
const [handleScroll, handleScrollToLive] = useHandleScroll(
|
const [handleScroll, handleScrollToLive] = useHandleScroll(
|
||||||
roomTimeline,
|
roomTimeline,
|
||||||
|
@ -417,7 +404,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
||||||
readUptoEvtStore,
|
readUptoEvtStore,
|
||||||
forceUpdateLimit,
|
forceUpdateLimit,
|
||||||
timelineScrollRef,
|
timelineScrollRef,
|
||||||
eventLimitRef,
|
eventLimitRef
|
||||||
);
|
);
|
||||||
const newEvent = useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, eventLimitRef);
|
const newEvent = useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, eventLimitRef);
|
||||||
|
|
||||||
|
@ -476,41 +463,46 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timelineScroll = timelineScrollRef.current;
|
const timelineScroll = timelineScrollRef.current;
|
||||||
if (!roomTimeline.initialized) return;
|
if (!roomTimeline.initialized) return;
|
||||||
if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward() && document.visibilityState === 'visible') {
|
if (
|
||||||
|
timelineScroll.bottom < 16 &&
|
||||||
|
!roomTimeline.canPaginateForward() &&
|
||||||
|
document.visibilityState === 'visible'
|
||||||
|
) {
|
||||||
timelineScroll.scrollToBottom();
|
timelineScroll.scrollToBottom();
|
||||||
} else {
|
} else {
|
||||||
timelineScroll.tryRestoringScroll();
|
timelineScroll.tryRestoringScroll();
|
||||||
}
|
}
|
||||||
}, [newEvent]);
|
}, [newEvent]);
|
||||||
|
|
||||||
const listenKeyboard = useCallback((event) => {
|
const listenKeyboard = useCallback(
|
||||||
if (event.ctrlKey || event.altKey || event.metaKey) return;
|
(event) => {
|
||||||
if (event.key !== 'ArrowUp') return;
|
if (event.ctrlKey || event.altKey || event.metaKey) return;
|
||||||
if (navigation.isRawModalVisible) return;
|
if (event.key !== 'ArrowUp') return;
|
||||||
|
if (navigation.isRawModalVisible) return;
|
||||||
|
|
||||||
if (document.activeElement.id !== 'message-textarea') return;
|
if (document.activeElement.id !== 'message-textarea') return;
|
||||||
if (document.activeElement.value !== '') return;
|
if (document.activeElement.value !== '') return;
|
||||||
|
|
||||||
const {
|
const { timeline: tl, activeTimeline, liveTimeline, matrixClient: mx } = roomTimeline;
|
||||||
timeline: tl, activeTimeline, liveTimeline, matrixClient: mx,
|
const limit = eventLimitRef.current;
|
||||||
} = roomTimeline;
|
if (activeTimeline !== liveTimeline) return;
|
||||||
const limit = eventLimitRef.current;
|
if (tl.length > limit.length) return;
|
||||||
if (activeTimeline !== liveTimeline) return;
|
|
||||||
if (tl.length > limit.length) return;
|
|
||||||
|
|
||||||
const mTypes = ['m.text'];
|
const mTypes = ['m.text'];
|
||||||
for (let i = tl.length - 1; i >= 0; i -= 1) {
|
for (let i = tl.length - 1; i >= 0; i -= 1) {
|
||||||
const mE = tl[i];
|
const mE = tl[i];
|
||||||
if (
|
if (
|
||||||
mE.getSender() === mx.getUserId()
|
mE.getSender() === mx.getUserId() &&
|
||||||
&& mE.getType() === 'm.room.message'
|
mE.getType() === 'm.room.message' &&
|
||||||
&& mTypes.includes(mE.getContent()?.msgtype)
|
mTypes.includes(mE.getContent()?.msgtype)
|
||||||
) {
|
) {
|
||||||
setEditEventId(mE.getId());
|
setEditEventId(mE.getId());
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}, [roomTimeline]);
|
[roomTimeline]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.body.addEventListener('keydown', listenKeyboard);
|
document.body.addEventListener('keydown', listenKeyboard);
|
||||||
|
@ -551,7 +543,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
||||||
if (i === 0 && !roomTimeline.canPaginateBackward()) {
|
if (i === 0 && !roomTimeline.canPaginateBackward()) {
|
||||||
if (mEvent.getType() === 'm.room.create') {
|
if (mEvent.getType() === 'm.room.create') {
|
||||||
tl.push(
|
tl.push(
|
||||||
<RoomIntroContainer key={mEvent.getId()} event={mEvent} timeline={roomTimeline} />,
|
<RoomIntroContainer key={mEvent.getId()} event={mEvent} timeline={roomTimeline} />
|
||||||
);
|
);
|
||||||
itemCountIndex += 1;
|
itemCountIndex += 1;
|
||||||
// eslint-disable-next-line no-continue
|
// eslint-disable-next-line no-continue
|
||||||
|
@ -564,9 +556,10 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
||||||
|
|
||||||
let isNewEvent = false;
|
let isNewEvent = false;
|
||||||
if (!unreadDivider) {
|
if (!unreadDivider) {
|
||||||
unreadDivider = (readUptoEvent
|
unreadDivider =
|
||||||
&& prevMEvent?.getTs() <= readUptoEvent.getTs()
|
readUptoEvent &&
|
||||||
&& readUptoEvent.getTs() < mEvent.getTs());
|
prevMEvent?.getTs() <= readUptoEvent.getTs() &&
|
||||||
|
readUptoEvent.getTs() < mEvent.getTs();
|
||||||
if (unreadDivider) {
|
if (unreadDivider) {
|
||||||
isNewEvent = true;
|
isNewEvent = true;
|
||||||
tl.push(<Divider key={`new-${mEvent.getId()}`} variant="positive" text="New messages" />);
|
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());
|
const dayDivider = prevMEvent && !isInSameDay(mEvent.getDate(), prevMEvent.getDate());
|
||||||
if (dayDivider) {
|
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;
|
itemCountIndex += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -584,15 +582,17 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
||||||
const isFocus = focusId === mEvent.getId();
|
const isFocus = focusId === mEvent.getId();
|
||||||
if (isFocus) jumpToItemIndex = itemCountIndex;
|
if (isFocus) jumpToItemIndex = itemCountIndex;
|
||||||
|
|
||||||
tl.push(renderEvent(
|
tl.push(
|
||||||
roomTimeline,
|
renderEvent(
|
||||||
mEvent,
|
roomTimeline,
|
||||||
isNewEvent ? null : prevMEvent,
|
mEvent,
|
||||||
isFocus,
|
isNewEvent ? null : prevMEvent,
|
||||||
editEventId === mEvent.getId(),
|
isFocus,
|
||||||
setEditEventId,
|
editEventId === mEvent.getId(),
|
||||||
cancelEdit,
|
setEditEventId,
|
||||||
));
|
cancelEdit
|
||||||
|
)
|
||||||
|
);
|
||||||
itemCountIndex += 1;
|
itemCountIndex += 1;
|
||||||
}
|
}
|
||||||
if (roomTimeline.canPaginateForward() || limit.length < timeline.length) {
|
if (roomTimeline.canPaginateForward() || limit.length < timeline.length) {
|
||||||
|
@ -606,7 +606,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
||||||
<ScrollView onScroll={handleTimelineScroll} ref={timelineSVRef} autoHide>
|
<ScrollView onScroll={handleTimelineScroll} ref={timelineSVRef} autoHide>
|
||||||
<div className="room-view__content" onClick={handleOnClickCapture}>
|
<div className="room-view__content" onClick={handleOnClickCapture}>
|
||||||
<div className="timeline__wrapper">
|
<div className="timeline__wrapper">
|
||||||
{ roomTimeline.initialized ? renderTimeline() : loadingMsgPlaceholders('loading', 3) }
|
{roomTimeline.initialized ? renderTimeline() : loadingMsgPlaceholders('loading', 3)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
|
@ -2,13 +2,16 @@ import React, { useEffect, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './RoomViewHeader.scss';
|
import './RoomViewHeader.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
import { blurOnBubbling } from '../../atoms/button/script';
|
import { blurOnBubbling } from '../../atoms/button/script';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
import { toggleRoomSettings, openReusableContextMenu, openNavigation } from '../../../client/action/navigation';
|
import {
|
||||||
|
toggleRoomSettings,
|
||||||
|
openReusableContextMenu,
|
||||||
|
openNavigation,
|
||||||
|
} from '../../../client/action/navigation';
|
||||||
import { togglePeopleDrawer } from '../../../client/action/settings';
|
import { togglePeopleDrawer } from '../../../client/action/settings';
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
import { getEventCords } from '../../../util/common';
|
import { getEventCords } from '../../../util/common';
|
||||||
|
@ -35,16 +38,16 @@ function RoomViewHeader({ roomId }) {
|
||||||
const isDM = initMatrix.roomList.directs.has(roomId);
|
const isDM = initMatrix.roomList.directs.has(roomId);
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
let avatarSrc = room.getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
|
let avatarSrc = room.getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
|
||||||
avatarSrc = isDM ? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop') : avatarSrc;
|
avatarSrc = isDM
|
||||||
|
? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop')
|
||||||
|
: avatarSrc;
|
||||||
const roomName = room.name;
|
const roomName = room.name;
|
||||||
|
|
||||||
const roomHeaderBtnRef = useRef(null);
|
const roomHeaderBtnRef = useRef(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const settingsToggle = (isVisibile) => {
|
const settingsToggle = (isVisibile) => {
|
||||||
const rawIcon = roomHeaderBtnRef.current.lastElementChild;
|
const rawIcon = roomHeaderBtnRef.current.lastElementChild;
|
||||||
rawIcon.style.transform = isVisibile
|
rawIcon.style.transform = isVisibile ? 'rotateX(180deg)' : 'rotateX(0deg)';
|
||||||
? 'rotateX(180deg)'
|
|
||||||
: 'rotateX(0deg)';
|
|
||||||
};
|
};
|
||||||
navigation.on(cons.events.navigation.ROOM_SETTINGS_TOGGLED, settingsToggle);
|
navigation.on(cons.events.navigation.ROOM_SETTINGS_TOGGLED, settingsToggle);
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -66,11 +69,9 @@ function RoomViewHeader({ roomId }) {
|
||||||
}, [roomId]);
|
}, [roomId]);
|
||||||
|
|
||||||
const openRoomOptions = (e) => {
|
const openRoomOptions = (e) => {
|
||||||
openReusableContextMenu(
|
openReusableContextMenu('bottom', getEventCords(e, '.ic-btn'), (closeMenu) => (
|
||||||
'bottom',
|
<RoomOptions roomId={roomId} afterOptionSelect={closeMenu} />
|
||||||
getEventCords(e, '.ic-btn'),
|
));
|
||||||
(closeMenu) => <RoomOptions roomId={roomId} afterOptionSelect={closeMenu} />,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -90,18 +91,32 @@ function RoomViewHeader({ roomId }) {
|
||||||
>
|
>
|
||||||
<Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="small" />
|
<Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="small" />
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
<Text variant="h2" weight="medium" primary>{twemojify(roomName)}</Text>
|
<Text variant="h2" weight="medium" primary>
|
||||||
|
{roomName}
|
||||||
|
</Text>
|
||||||
</TitleWrapper>
|
</TitleWrapper>
|
||||||
<RawIcon src={ChevronBottomIC} />
|
<RawIcon src={ChevronBottomIC} />
|
||||||
</button>
|
</button>
|
||||||
{mx.isRoomEncrypted(roomId) === false && <IconButton onClick={() => toggleRoomSettings(tabText.SEARCH)} tooltip="Search" src={SearchIC} />}
|
{mx.isRoomEncrypted(roomId) === false && (
|
||||||
<IconButton className="room-header__drawer-btn" onClick={togglePeopleDrawer} tooltip="People" src={UserIC} />
|
<IconButton
|
||||||
<IconButton className="room-header__members-btn" onClick={() => toggleRoomSettings(tabText.MEMBERS)} tooltip="Members" src={UserIC} />
|
onClick={() => toggleRoomSettings(tabText.SEARCH)}
|
||||||
|
tooltip="Search"
|
||||||
|
src={SearchIC}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={openRoomOptions}
|
className="room-header__drawer-btn"
|
||||||
tooltip="Options"
|
onClick={togglePeopleDrawer}
|
||||||
src={VerticalMenuIC}
|
tooltip="People"
|
||||||
|
src={UserIC}
|
||||||
/>
|
/>
|
||||||
|
<IconButton
|
||||||
|
className="room-header__members-btn"
|
||||||
|
onClick={() => toggleRoomSettings(tabText.MEMBERS)}
|
||||||
|
tooltip="Members"
|
||||||
|
src={UserIC}
|
||||||
|
/>
|
||||||
|
<IconButton onClick={openRoomOptions} tooltip="Options" src={VerticalMenuIC} />
|
||||||
</Header>
|
</Header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil';
|
import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil';
|
||||||
|
|
||||||
|
@ -10,83 +8,83 @@ function getTimelineJSXMessages() {
|
||||||
join(user) {
|
join(user) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>{twemojify(user)}</b>
|
<b>{user}</b>
|
||||||
{' joined the room'}
|
{' joined the room'}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
leave(user, reason) {
|
leave(user, reason) {
|
||||||
const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
|
const reasonMsg = typeof reason === 'string' ? `: ${reason}` : '';
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>{twemojify(user)}</b>
|
<b>{user}</b>
|
||||||
{' left the room'}
|
{' left the room'}
|
||||||
{twemojify(reasonMsg)}
|
{reasonMsg}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
invite(inviter, user) {
|
invite(inviter, user) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>{twemojify(inviter)}</b>
|
<b>{inviter}</b>
|
||||||
{' invited '}
|
{' invited '}
|
||||||
<b>{twemojify(user)}</b>
|
<b>{user}</b>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
cancelInvite(inviter, user) {
|
cancelInvite(inviter, user) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>{twemojify(inviter)}</b>
|
<b>{inviter}</b>
|
||||||
{' canceled '}
|
{' canceled '}
|
||||||
<b>{twemojify(user)}</b>
|
<b>{user}</b>
|
||||||
{'\'s invite'}
|
{"'s invite"}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
rejectInvite(user) {
|
rejectInvite(user) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>{twemojify(user)}</b>
|
<b>{user}</b>
|
||||||
{' rejected the invitation'}
|
{' rejected the invitation'}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
kick(actor, user, reason) {
|
kick(actor, user, reason) {
|
||||||
const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
|
const reasonMsg = typeof reason === 'string' ? `: ${reason}` : '';
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>{twemojify(actor)}</b>
|
<b>{actor}</b>
|
||||||
{' kicked '}
|
{' kicked '}
|
||||||
<b>{twemojify(user)}</b>
|
<b>{user}</b>
|
||||||
{twemojify(reasonMsg)}
|
{reasonMsg}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
ban(actor, user, reason) {
|
ban(actor, user, reason) {
|
||||||
const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
|
const reasonMsg = typeof reason === 'string' ? `: ${reason}` : '';
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>{twemojify(actor)}</b>
|
<b>{actor}</b>
|
||||||
{' banned '}
|
{' banned '}
|
||||||
<b>{twemojify(user)}</b>
|
<b>{user}</b>
|
||||||
{twemojify(reasonMsg)}
|
{reasonMsg}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
unban(actor, user) {
|
unban(actor, user) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>{twemojify(actor)}</b>
|
<b>{actor}</b>
|
||||||
{' unbanned '}
|
{' unbanned '}
|
||||||
<b>{twemojify(user)}</b>
|
<b>{user}</b>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
avatarSets(user) {
|
avatarSets(user) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>{twemojify(user)}</b>
|
<b>{user}</b>
|
||||||
{' set a avatar'}
|
{' set a avatar'}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -94,7 +92,7 @@ function getTimelineJSXMessages() {
|
||||||
avatarChanged(user) {
|
avatarChanged(user) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>{twemojify(user)}</b>
|
<b>{user}</b>
|
||||||
{' changed their avatar'}
|
{' changed their avatar'}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -102,7 +100,7 @@ function getTimelineJSXMessages() {
|
||||||
avatarRemoved(user) {
|
avatarRemoved(user) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>{twemojify(user)}</b>
|
<b>{user}</b>
|
||||||
{' removed their avatar'}
|
{' removed their avatar'}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -110,27 +108,27 @@ function getTimelineJSXMessages() {
|
||||||
nameSets(user, newName) {
|
nameSets(user, newName) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>{twemojify(user)}</b>
|
<b>{user}</b>
|
||||||
{' set display name to '}
|
{' set display name to '}
|
||||||
<b>{twemojify(newName)}</b>
|
<b>{newName}</b>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
nameChanged(user, newName) {
|
nameChanged(user, newName) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>{twemojify(user)}</b>
|
<b>{user}</b>
|
||||||
{' changed their display name to '}
|
{' changed their display name to '}
|
||||||
<b>{twemojify(newName)}</b>
|
<b>{newName}</b>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
nameRemoved(user, lastName) {
|
nameRemoved(user, lastName) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>{twemojify(user)}</b>
|
<b>{user}</b>
|
||||||
{' removed their display name '}
|
{' 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));
|
if (room?.getMember(userId)) return getUsernameOfRoomMember(room.getMember(userId));
|
||||||
return getUsername(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 (!Array.isArray(userIds)) return 'Idle';
|
||||||
if (userIds.length === 0) return 'Idle';
|
if (userIds.length === 0) return 'Idle';
|
||||||
const MAX_VISIBLE_COUNT = 3;
|
const MAX_VISIBLE_COUNT = 3;
|
||||||
|
|
||||||
const u1Jsx = getUserJSX(userIds[0]);
|
const u1Jsx = getUserJSX(userIds[0]);
|
||||||
// eslint-disable-next-line react/jsx-one-expression-per-line
|
// 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]);
|
const u2Jsx = getUserJSX(userIds[1]);
|
||||||
// eslint-disable-next-line react/jsx-one-expression-per-line
|
// 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]);
|
const u3Jsx = getUserJSX(userIds[2]);
|
||||||
if (userIds.length === 3) {
|
if (userIds.length === 3) {
|
||||||
// eslint-disable-next-line react/jsx-one-expression-per-line
|
// 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;
|
const othersCount = userIds.length - MAX_VISIBLE_COUNT;
|
||||||
// eslint-disable-next-line react/jsx-one-expression-per-line
|
// 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) {
|
function parseTimelineChange(mEvent) {
|
||||||
|
@ -180,18 +196,27 @@ function parseTimelineChange(mEvent) {
|
||||||
const userName = getUsername(mEvent.getStateKey());
|
const userName = getUsername(mEvent.getStateKey());
|
||||||
|
|
||||||
switch (content.membership) {
|
switch (content.membership) {
|
||||||
case 'invite': return makeReturnObj('invite', tJSXMsgs.invite(senderName, userName));
|
case 'invite':
|
||||||
case 'ban': return makeReturnObj('leave', tJSXMsgs.ban(senderName, userName, content.reason));
|
return makeReturnObj('invite', tJSXMsgs.invite(senderName, userName));
|
||||||
|
case 'ban':
|
||||||
|
return makeReturnObj('leave', tJSXMsgs.ban(senderName, userName, content.reason));
|
||||||
case 'join':
|
case 'join':
|
||||||
if (prevContent.membership === 'join') {
|
if (prevContent.membership === 'join') {
|
||||||
if (content.displayname !== prevContent.displayname) {
|
if (content.displayname !== prevContent.displayname) {
|
||||||
if (typeof content.displayname === 'undefined') return makeReturnObj('avatar', tJSXMsgs.nameRemoved(sender, prevContent.displayname));
|
if (typeof content.displayname === 'undefined')
|
||||||
if (typeof prevContent.displayname === 'undefined') return makeReturnObj('avatar', tJSXMsgs.nameSets(sender, content.displayname));
|
return makeReturnObj('avatar', tJSXMsgs.nameRemoved(sender, prevContent.displayname));
|
||||||
return makeReturnObj('avatar', tJSXMsgs.nameChanged(prevContent.displayname, content.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 (content.avatar_url !== prevContent.avatar_url) {
|
||||||
if (typeof content.avatar_url === 'undefined') return makeReturnObj('avatar', tJSXMsgs.avatarRemoved(content.displayname));
|
if (typeof content.avatar_url === 'undefined')
|
||||||
if (typeof prevContent.avatar_url === 'undefined') return makeReturnObj('avatar', tJSXMsgs.avatarSets(content.displayname));
|
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 makeReturnObj('avatar', tJSXMsgs.avatarChanged(content.displayname));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -200,23 +225,25 @@ function parseTimelineChange(mEvent) {
|
||||||
case 'leave':
|
case 'leave':
|
||||||
if (sender === mEvent.getStateKey()) {
|
if (sender === mEvent.getStateKey()) {
|
||||||
switch (prevContent.membership) {
|
switch (prevContent.membership) {
|
||||||
case 'invite': return makeReturnObj('invite-cancel', tJSXMsgs.rejectInvite(senderName));
|
case 'invite':
|
||||||
default: return makeReturnObj('leave', tJSXMsgs.leave(senderName, content.reason));
|
return makeReturnObj('invite-cancel', tJSXMsgs.rejectInvite(senderName));
|
||||||
|
default:
|
||||||
|
return makeReturnObj('leave', tJSXMsgs.leave(senderName, content.reason));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch (prevContent.membership) {
|
switch (prevContent.membership) {
|
||||||
case 'invite': return makeReturnObj('invite-cancel', tJSXMsgs.cancelInvite(senderName, userName));
|
case 'invite':
|
||||||
case 'ban': return makeReturnObj('other', tJSXMsgs.unban(senderName, userName));
|
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,
|
// sender is not target and made the target leave,
|
||||||
// if not from invite/ban then this is a kick
|
// 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 {
|
export { getTimelineJSXMessages, getUsersActionJsx, parseTimelineChange };
|
||||||
getTimelineJSXMessages,
|
|
||||||
getUsersActionJsx,
|
|
||||||
parseTimelineChange,
|
|
||||||
};
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ import React, { useState } from 'react';
|
||||||
import './CrossSigning.scss';
|
import './CrossSigning.scss';
|
||||||
import FileSaver from 'file-saver';
|
import FileSaver from 'file-saver';
|
||||||
import { Formik } from 'formik';
|
import { Formik } from 'formik';
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import { openReusableDialog } from '../../../client/action/navigation';
|
import { openReusableDialog } from '../../../client/action/navigation';
|
||||||
|
@ -22,15 +21,17 @@ import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
|
||||||
const failedDialog = () => {
|
const failedDialog = () => {
|
||||||
const renderFailure = (requestClose) => (
|
const renderFailure = (requestClose) => (
|
||||||
<div className="cross-signing__failure">
|
<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>
|
<Text weight="medium">Failed to setup cross signing. Please try again.</Text>
|
||||||
<Button onClick={requestClose}>Close</Button>
|
<Button onClick={requestClose}>Close</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
openReusableDialog(
|
openReusableDialog(
|
||||||
<Text variant="s1" weight="medium">Setup cross signing</Text>,
|
<Text variant="s1" weight="medium">
|
||||||
renderFailure,
|
Setup cross signing
|
||||||
|
</Text>,
|
||||||
|
renderFailure
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -48,11 +49,11 @@ const securityKeyDialog = (key) => {
|
||||||
const renderSecurityKey = () => (
|
const renderSecurityKey = () => (
|
||||||
<div className="cross-signing__key">
|
<div className="cross-signing__key">
|
||||||
<Text weight="medium">Please save this security key somewhere safe.</Text>
|
<Text weight="medium">Please save this security key somewhere safe.</Text>
|
||||||
<Text className="cross-signing__key-text">
|
<Text className="cross-signing__key-text">{key.encodedPrivateKey}</Text>
|
||||||
{key.encodedPrivateKey}
|
|
||||||
</Text>
|
|
||||||
<div className="cross-signing__key-btn">
|
<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>
|
<Button onClick={() => downloadKey(key)}>Download</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,8 +63,10 @@ const securityKeyDialog = (key) => {
|
||||||
downloadKey();
|
downloadKey();
|
||||||
|
|
||||||
openReusableDialog(
|
openReusableDialog(
|
||||||
<Text variant="s1" weight="medium">Security Key</Text>,
|
<Text variant="s1" weight="medium">
|
||||||
() => renderSecurityKey(),
|
Security Key
|
||||||
|
</Text>,
|
||||||
|
() => renderSecurityKey()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -112,7 +115,7 @@ function CrossSigningSetup() {
|
||||||
errors.phrase = 'Phrase must contain 8-127 characters with no space.';
|
errors.phrase = 'Phrase must contain 8-127 characters with no space.';
|
||||||
}
|
}
|
||||||
if (values.confirmPhrase.length > 0 && values.confirmPhrase !== values.phrase) {
|
if (values.confirmPhrase.length > 0 && values.confirmPhrase !== values.phrase) {
|
||||||
errors.confirmPhrase = 'Phrase don\'t match.';
|
errors.confirmPhrase = "Phrase don't match.";
|
||||||
}
|
}
|
||||||
return errors;
|
return errors;
|
||||||
};
|
};
|
||||||
|
@ -121,10 +124,14 @@ function CrossSigningSetup() {
|
||||||
<div className="cross-signing__setup">
|
<div className="cross-signing__setup">
|
||||||
<div className="cross-signing__setup-entry">
|
<div className="cross-signing__setup-entry">
|
||||||
<Text>
|
<Text>
|
||||||
We will generate a <b>Security Key</b>,
|
We will generate a <b>Security Key</b>, which you can use to manage messages backup and
|
||||||
which you can use to manage messages backup and session verification.
|
session verification.
|
||||||
</Text>
|
</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" />}
|
{genWithPhrase === false && <Spinner size="small" />}
|
||||||
</div>
|
</div>
|
||||||
<Text className="cross-signing__setup-divider">OR</Text>
|
<Text className="cross-signing__setup-divider">OR</Text>
|
||||||
|
@ -133,9 +140,7 @@ function CrossSigningSetup() {
|
||||||
onSubmit={(values) => setup(values.phrase)}
|
onSubmit={(values) => setup(values.phrase)}
|
||||||
validate={validator}
|
validate={validator}
|
||||||
>
|
>
|
||||||
{({
|
{({ values, errors, handleChange, handleSubmit }) => (
|
||||||
values, errors, handleChange, handleSubmit,
|
|
||||||
}) => (
|
|
||||||
<form
|
<form
|
||||||
className="cross-signing__setup-entry"
|
className="cross-signing__setup-entry"
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
|
@ -143,8 +148,8 @@ function CrossSigningSetup() {
|
||||||
>
|
>
|
||||||
<Text>
|
<Text>
|
||||||
Alternatively you can also set a <b>Security Phrase </b>
|
Alternatively you can also set a <b>Security Phrase </b>
|
||||||
so you don't have to remember long Security Key,
|
so you don't have to remember long Security Key, and optionally save the Key as
|
||||||
and optionally save the Key as backup.
|
backup.
|
||||||
</Text>
|
</Text>
|
||||||
<Input
|
<Input
|
||||||
name="phrase"
|
name="phrase"
|
||||||
|
@ -155,7 +160,11 @@ function CrossSigningSetup() {
|
||||||
required
|
required
|
||||||
disabled={genWithPhrase !== undefined}
|
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
|
<Input
|
||||||
name="confirmPhrase"
|
name="confirmPhrase"
|
||||||
value={values.confirmPhrase}
|
value={values.confirmPhrase}
|
||||||
|
@ -165,8 +174,16 @@ function CrossSigningSetup() {
|
||||||
required
|
required
|
||||||
disabled={genWithPhrase !== undefined}
|
disabled={genWithPhrase !== undefined}
|
||||||
/>
|
/>
|
||||||
{errors.confirmPhrase && <Text variant="b3" className="cross-signing__error">{errors.confirmPhrase}</Text>}
|
{errors.confirmPhrase && (
|
||||||
{genWithPhrase !== true && <Button variant="primary" type="submit" disabled={genWithPhrase !== undefined}>Set Phrase & Generate Key</Button>}
|
<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" />}
|
{genWithPhrase === true && <Spinner size="small" />}
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
@ -177,31 +194,36 @@ function CrossSigningSetup() {
|
||||||
|
|
||||||
const setupDialog = () => {
|
const setupDialog = () => {
|
||||||
openReusableDialog(
|
openReusableDialog(
|
||||||
<Text variant="s1" weight="medium">Setup cross signing</Text>,
|
<Text variant="s1" weight="medium">
|
||||||
() => <CrossSigningSetup />,
|
Setup cross signing
|
||||||
|
</Text>,
|
||||||
|
() => <CrossSigningSetup />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function CrossSigningReset() {
|
function CrossSigningReset() {
|
||||||
return (
|
return (
|
||||||
<div className="cross-signing__reset">
|
<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 weight="medium">Resetting cross-signing keys is permanent.</Text>
|
||||||
<Text>
|
<Text>
|
||||||
Anyone you have verified with will see security alerts and your message backup will be lost.
|
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,
|
You almost certainly do not want to do this, unless you have lost <b>Security Key</b> or{' '}
|
||||||
unless you have lost <b>Security Key</b> or <b>Phrase</b> and
|
<b>Phrase</b> and every session you can cross-sign from.
|
||||||
every session you can cross-sign from.
|
|
||||||
</Text>
|
</Text>
|
||||||
<Button variant="danger" onClick={setupDialog}>Reset</Button>
|
<Button variant="danger" onClick={setupDialog}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetDialog = () => {
|
const resetDialog = () => {
|
||||||
openReusableDialog(
|
openReusableDialog(
|
||||||
<Text variant="s1" weight="medium">Reset cross signing</Text>,
|
<Text variant="s1" weight="medium">
|
||||||
() => <CrossSigningReset />,
|
Reset cross signing
|
||||||
|
</Text>,
|
||||||
|
() => <CrossSigningReset />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -210,12 +232,23 @@ function CrossSignin() {
|
||||||
return (
|
return (
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Cross signing"
|
title="Cross signing"
|
||||||
content={<Text variant="b3">Setup to verify and keep track of all your sessions. Also required to backup encrypted message.</Text>}
|
content={
|
||||||
options={(
|
<Text variant="b3">
|
||||||
isCSEnabled
|
Setup to verify and keep track of all your sessions. Also required to backup encrypted
|
||||||
? <Button variant="danger" onClick={resetDialog}>Reset</Button>
|
message.
|
||||||
: <Button variant="primary" onClick={setupDialog}>Setup</Button>
|
</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 React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './KeyBackup.scss';
|
import './KeyBackup.scss';
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import { openReusableDialog } from '../../../client/action/navigation';
|
import { openReusableDialog } from '../../../client/action/navigation';
|
||||||
|
@ -34,10 +33,7 @@ function CreateKeyBackupDialog({ keyData }) {
|
||||||
let info;
|
let info;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
info = await mx.prepareKeyBackupVersion(
|
info = await mx.prepareKeyBackupVersion(null, { secureSecretStorage: true });
|
||||||
null,
|
|
||||||
{ secureSecretStorage: true },
|
|
||||||
);
|
|
||||||
info = await mx.createKeyBackupVersion(info);
|
info = await mx.createKeyBackupVersion(info);
|
||||||
await mx.scheduleAllGroupSessionsForBackup();
|
await mx.scheduleAllGroupSessionsForBackup();
|
||||||
if (!mountStore.getItem()) return;
|
if (!mountStore.getItem()) return;
|
||||||
|
@ -65,7 +61,7 @@ function CreateKeyBackupDialog({ keyData }) {
|
||||||
)}
|
)}
|
||||||
{done === true && (
|
{done === true && (
|
||||||
<>
|
<>
|
||||||
<Text variant="h1">{twemojify('✅')}</Text>
|
<Text variant="h1">{'✅'}</Text>
|
||||||
<Text>Successfully created backup</Text>
|
<Text>Successfully created backup</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -104,12 +100,9 @@ function RestoreKeyBackupDialog({ keyData }) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const backupInfo = await mx.getKeyBackupVersion();
|
const backupInfo = await mx.getKeyBackupVersion();
|
||||||
const info = await mx.restoreKeyBackupWithSecretStorage(
|
const info = await mx.restoreKeyBackupWithSecretStorage(backupInfo, undefined, undefined, {
|
||||||
backupInfo,
|
progressCallback,
|
||||||
undefined,
|
});
|
||||||
undefined,
|
|
||||||
{ progressCallback },
|
|
||||||
);
|
|
||||||
if (!mountStore.getItem()) return;
|
if (!mountStore.getItem()) return;
|
||||||
setStatus({ done: `Successfully restored backup keys (${info.imported}/${info.total}).` });
|
setStatus({ done: `Successfully restored backup keys (${info.imported}/${info.total}).` });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -138,7 +131,7 @@ function RestoreKeyBackupDialog({ keyData }) {
|
||||||
)}
|
)}
|
||||||
{status.done && (
|
{status.done && (
|
||||||
<>
|
<>
|
||||||
<Text variant="h1">{twemojify('✅')}</Text>
|
<Text variant="h1">{'✅'}</Text>
|
||||||
<Text>{status.done}</Text>
|
<Text>{status.done}</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -176,14 +169,16 @@ function DeleteKeyBackupDialog({ requestClose }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="key-backup__delete">
|
<div className="key-backup__delete">
|
||||||
<Text variant="h1">{twemojify('🗑')}</Text>
|
<Text variant="h1">{'🗑'}</Text>
|
||||||
<Text weight="medium">Deleting key backup is permanent.</Text>
|
<Text weight="medium">Deleting key backup is permanent.</Text>
|
||||||
<Text>All encrypted messages keys stored on server will be deleted.</Text>
|
<Text>All encrypted messages keys stored on server will be deleted.</Text>
|
||||||
{
|
{isDeleting ? (
|
||||||
isDeleting
|
<Spinner size="small" />
|
||||||
? <Spinner size="small" />
|
) : (
|
||||||
: <Button variant="danger" onClick={deleteBackup}>Delete</Button>
|
<Button variant="danger" onClick={deleteBackup}>
|
||||||
}
|
Delete
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -224,9 +219,11 @@ function KeyBackup() {
|
||||||
if (keyData === null) return;
|
if (keyData === null) return;
|
||||||
|
|
||||||
openReusableDialog(
|
openReusableDialog(
|
||||||
<Text variant="s1" weight="medium">Create Key Backup</Text>,
|
<Text variant="s1" weight="medium">
|
||||||
|
Create Key Backup
|
||||||
|
</Text>,
|
||||||
() => <CreateKeyBackupDialog keyData={keyData} />,
|
() => <CreateKeyBackupDialog keyData={keyData} />,
|
||||||
() => fetchKeyBackupVersion(),
|
() => fetchKeyBackupVersion()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -235,29 +232,44 @@ function KeyBackup() {
|
||||||
if (keyData === null) return;
|
if (keyData === null) return;
|
||||||
|
|
||||||
openReusableDialog(
|
openReusableDialog(
|
||||||
<Text variant="s1" weight="medium">Restore Key Backup</Text>,
|
<Text variant="s1" weight="medium">
|
||||||
() => <RestoreKeyBackupDialog keyData={keyData} />,
|
Restore Key Backup
|
||||||
|
</Text>,
|
||||||
|
() => <RestoreKeyBackupDialog keyData={keyData} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openDeleteKeyBackup = () => openReusableDialog(
|
const openDeleteKeyBackup = () =>
|
||||||
<Text variant="s1" weight="medium">Delete Key Backup</Text>,
|
openReusableDialog(
|
||||||
(requestClose) => (
|
<Text variant="s1" weight="medium">
|
||||||
<DeleteKeyBackupDialog
|
Delete Key Backup
|
||||||
requestClose={(isDone) => {
|
</Text>,
|
||||||
if (isDone) setKeyBackup(null);
|
(requestClose) => (
|
||||||
requestClose();
|
<DeleteKeyBackupDialog
|
||||||
}}
|
requestClose={(isDone) => {
|
||||||
/>
|
if (isDone) setKeyBackup(null);
|
||||||
),
|
requestClose();
|
||||||
);
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const renderOptions = () => {
|
const renderOptions = () => {
|
||||||
if (keyBackup === undefined) return <Spinner size="small" />;
|
if (keyBackup === undefined) return <Spinner size="small" />;
|
||||||
if (keyBackup === null) return <Button variant="primary" onClick={openCreateKeyBackup}>Create Backup</Button>;
|
if (keyBackup === null)
|
||||||
|
return (
|
||||||
|
<Button variant="primary" onClick={openCreateKeyBackup}>
|
||||||
|
Create Backup
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IconButton src={DownloadIC} variant="positive" onClick={openRestoreKeyBackup} tooltip="Restore backup" />
|
<IconButton
|
||||||
|
src={DownloadIC}
|
||||||
|
variant="positive"
|
||||||
|
onClick={openRestoreKeyBackup}
|
||||||
|
tooltip="Restore backup"
|
||||||
|
/>
|
||||||
<IconButton src={BinIC} onClick={openDeleteKeyBackup} tooltip="Delete backup" />
|
<IconButton src={BinIC} onClick={openDeleteKeyBackup} tooltip="Delete backup" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -266,9 +278,12 @@ function KeyBackup() {
|
||||||
return (
|
return (
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Encrypted messages backup"
|
title="Encrypted messages backup"
|
||||||
content={(
|
content={
|
||||||
<>
|
<>
|
||||||
<Text variant="b3">Online backup your encrypted messages keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.</Text>
|
<Text variant="b3">
|
||||||
|
Online backup your encrypted messages keys with your account data in case you lose
|
||||||
|
access to your sessions. Your keys will be secured with a unique Security Key.
|
||||||
|
</Text>
|
||||||
{!isCSEnabled && (
|
{!isCSEnabled && (
|
||||||
<InfoCard
|
<InfoCard
|
||||||
style={{ marginTop: 'var(--sp-ultra-tight)' }}
|
style={{ marginTop: 'var(--sp-ultra-tight)' }}
|
||||||
|
@ -279,7 +294,7 @@ function KeyBackup() {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
}
|
||||||
options={isCSEnabled ? renderOptions() : null}
|
options={isCSEnabled ? renderOptions() : null}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -80,7 +80,7 @@ function AppearanceSection() {
|
||||||
{ text: 'Butter' },
|
{ text: 'Butter' },
|
||||||
{ text: 'Nord Dark' },
|
{ text: 'Nord Dark' },
|
||||||
{ text: 'Cyberpunk' },
|
{ text: 'Cyberpunk' },
|
||||||
{ text: 'Almond Dark'},
|
{ text: 'Almond Dark' },
|
||||||
]}
|
]}
|
||||||
onSelect={(index) => {
|
onSelect={(index) => {
|
||||||
if (settings.useSystemTheme) toggleSystemTheme();
|
if (settings.useSystemTheme) toggleSystemTheme();
|
||||||
|
@ -328,28 +328,6 @@ function AboutSection() {
|
||||||
.
|
.
|
||||||
</Text>
|
</Text>
|
||||||
</li>
|
</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>
|
<li>
|
||||||
{/* eslint-disable-next-line react/jsx-one-expression-per-line */}
|
{/* eslint-disable-next-line react/jsx-one-expression-per-line */}
|
||||||
<Text>
|
<Text>
|
||||||
|
|
|
@ -3,8 +3,6 @@ import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './SpaceManage.scss';
|
import './SpaceManage.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
|
@ -37,32 +35,37 @@ function SpaceManageBreadcrumb({ path, onSelect }) {
|
||||||
<div className="space-manage-breadcrumb__wrapper">
|
<div className="space-manage-breadcrumb__wrapper">
|
||||||
<ScrollView horizontal vertical={false} invisible>
|
<ScrollView horizontal vertical={false} invisible>
|
||||||
<div className="space-manage-breadcrumb">
|
<div className="space-manage-breadcrumb">
|
||||||
{
|
{path.map((item, index) => (
|
||||||
path.map((item, index) => (
|
<React.Fragment key={item.roomId}>
|
||||||
<React.Fragment key={item.roomId}>
|
{index > 0 && <RawIcon size="extra-small" src={ChevronRightIC} />}
|
||||||
{index > 0 && <RawIcon size="extra-small" src={ChevronRightIC} />}
|
<Button onClick={() => onSelect(item.roomId, item.name)}>
|
||||||
<Button onClick={() => onSelect(item.roomId, item.name)}>
|
<Text variant="b2">{item.name}</Text>
|
||||||
<Text variant="b2">{twemojify(item.name)}</Text>
|
</Button>
|
||||||
</Button>
|
</React.Fragment>
|
||||||
</React.Fragment>
|
))}
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
SpaceManageBreadcrumb.propTypes = {
|
SpaceManageBreadcrumb.propTypes = {
|
||||||
path: PropTypes.arrayOf(PropTypes.exact({
|
path: PropTypes.arrayOf(
|
||||||
roomId: PropTypes.string,
|
PropTypes.exact({
|
||||||
name: PropTypes.string,
|
roomId: PropTypes.string,
|
||||||
})).isRequired,
|
name: PropTypes.string,
|
||||||
|
})
|
||||||
|
).isRequired,
|
||||||
onSelect: PropTypes.func.isRequired,
|
onSelect: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
function SpaceManageItem({
|
function SpaceManageItem({
|
||||||
parentId, roomInfo, onSpaceClick, requestClose,
|
parentId,
|
||||||
isSelected, onSelect, roomHierarchy,
|
roomInfo,
|
||||||
|
onSpaceClick,
|
||||||
|
requestClose,
|
||||||
|
isSelected,
|
||||||
|
onSelect,
|
||||||
|
roomHierarchy,
|
||||||
}) {
|
}) {
|
||||||
const [isExpand, setIsExpand] = useState(false);
|
const [isExpand, setIsExpand] = useState(false);
|
||||||
const [isJoining, setIsJoining] = useState(false);
|
const [isJoining, setIsJoining] = useState(false);
|
||||||
|
@ -72,8 +75,11 @@ function SpaceManageItem({
|
||||||
const parentRoom = mx.getRoom(parentId);
|
const parentRoom = mx.getRoom(parentId);
|
||||||
const isSpace = roomInfo.room_type === 'm.space';
|
const isSpace = roomInfo.room_type === 'm.space';
|
||||||
const roomId = roomInfo.room_id;
|
const roomId = roomInfo.room_id;
|
||||||
const canManage = parentRoom?.currentState.maySendStateEvent('m.space.child', mx.getUserId()) || false;
|
const canManage =
|
||||||
const isSuggested = parentRoom?.currentState.getStateEvents('m.space.child', roomId)?.getContent().suggested === true;
|
parentRoom?.currentState.maySendStateEvent('m.space.child', mx.getUserId()) || false;
|
||||||
|
const isSuggested =
|
||||||
|
parentRoom?.currentState.getStateEvents('m.space.child', roomId)?.getContent().suggested ===
|
||||||
|
true;
|
||||||
|
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
const isJoined = !!(room?.getMyMembership() === 'join' || null);
|
const isJoined = !!(room?.getMyMembership() === 'join' || null);
|
||||||
|
@ -103,17 +109,13 @@ function SpaceManageItem({
|
||||||
bgColor={colorMXID(roomId)}
|
bgColor={colorMXID(roomId)}
|
||||||
imageSrc={isDM ? imageSrc : null}
|
imageSrc={isDM ? imageSrc : null}
|
||||||
iconColor="var(--ic-surface-low)"
|
iconColor="var(--ic-surface-low)"
|
||||||
iconSrc={
|
iconSrc={isDM ? null : joinRuleToIconSrc(roomInfo.join_rules || roomInfo.join_rule, isSpace)}
|
||||||
isDM
|
|
||||||
? null
|
|
||||||
: joinRuleToIconSrc((roomInfo.join_rules || roomInfo.join_rule), isSpace)
|
|
||||||
}
|
|
||||||
size="extra-small"
|
size="extra-small"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const roomNameJSX = (
|
const roomNameJSX = (
|
||||||
<Text>
|
<Text>
|
||||||
{twemojify(name)}
|
{name}
|
||||||
<Text variant="b3" span>{` • ${roomInfo.num_joined_members} members`}</Text>
|
<Text variant="b3" span>{` • ${roomInfo.num_joined_members} members`}</Text>
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
@ -130,11 +132,11 @@ function SpaceManageItem({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`space-manage-item${isSpace ? '--space' : ''}`}>
|
||||||
className={`space-manage-item${isSpace ? '--space' : ''}`}
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
{canManage && <Checkbox isActive={isSelected} onToggle={() => onSelect(roomId)} variant="positive" />}
|
{canManage && (
|
||||||
|
<Checkbox isActive={isSelected} onToggle={() => onSelect(roomId)} variant="positive" />
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
className="space-manage-item__btn"
|
className="space-manage-item__btn"
|
||||||
onClick={isSpace ? () => onSpaceClick(roomId, name) : null}
|
onClick={isSpace ? () => onSpaceClick(roomId, name) : null}
|
||||||
|
@ -145,13 +147,15 @@ function SpaceManageItem({
|
||||||
{isSuggested && <Text variant="b2">Suggested</Text>}
|
{isSuggested && <Text variant="b2">Suggested</Text>}
|
||||||
</button>
|
</button>
|
||||||
{roomInfo.topic && expandBtnJsx}
|
{roomInfo.topic && expandBtnJsx}
|
||||||
{
|
{isJoined ? (
|
||||||
isJoined
|
<Button onClick={handleOpen}>Open</Button>
|
||||||
? <Button onClick={handleOpen}>Open</Button>
|
) : (
|
||||||
: <Button variant="primary" onClick={handleJoin} disabled={isJoining}>{isJoining ? 'Joining...' : 'Join'}</Button>
|
<Button variant="primary" onClick={handleJoin} disabled={isJoining}>
|
||||||
}
|
{isJoining ? 'Joining...' : 'Join'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isExpand && roomInfo.topic && <Text variant="b2">{twemojify(roomInfo.topic, undefined, true)}</Text>}
|
{isExpand && roomInfo.topic && <Text variant="b2">{roomInfo.topic}</Text>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -201,9 +205,11 @@ function SpaceManageFooter({ parentId, selected }) {
|
||||||
<div className="space-manage__footer">
|
<div className="space-manage__footer">
|
||||||
{process && <Spinner size="small" />}
|
{process && <Spinner size="small" />}
|
||||||
<Text weight="medium">{process || `${selected.length} item selected`}</Text>
|
<Text weight="medium">{process || `${selected.length} item selected`}</Text>
|
||||||
{ !process && (
|
{!process && (
|
||||||
<>
|
<>
|
||||||
<Button onClick={handleRemove} variant="danger">Remove</Button>
|
<Button onClick={handleRemove} variant="danger">
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleToggleSuggested(!allSuggested)}
|
onClick={() => handleToggleSuggested(!allSuggested)}
|
||||||
variant={allSuggested ? 'surface' : 'primary'}
|
variant={allSuggested ? 'surface' : 'primary'}
|
||||||
|
@ -336,20 +342,19 @@ function SpaceManageContent({ roomId, requestClose }) {
|
||||||
if (!currentHierarchy) loadRoomHierarchy();
|
if (!currentHierarchy) loadRoomHierarchy();
|
||||||
return (
|
return (
|
||||||
<div className="space-manage__content">
|
<div className="space-manage__content">
|
||||||
{spacePath.length > 1 && (
|
{spacePath.length > 1 && <SpaceManageBreadcrumb path={spacePath} onSelect={addPathItem} />}
|
||||||
<SpaceManageBreadcrumb path={spacePath} onSelect={addPathItem} />
|
<Text variant="b3" weight="bold">
|
||||||
)}
|
Rooms and spaces
|
||||||
<Text variant="b3" weight="bold">Rooms and spaces</Text>
|
</Text>
|
||||||
<div className="space-manage__content-items">
|
<div className="space-manage__content-items">
|
||||||
{!isLoading && currentHierarchy?.rooms?.length === 1 && (
|
{!isLoading && currentHierarchy?.rooms?.length === 1 && (
|
||||||
<Text>
|
<Text>
|
||||||
Either the space contains private rooms or you need to join space to view it's rooms.
|
Either the space contains private rooms or you need to join space to view it's rooms.
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{currentHierarchy && (currentHierarchy.rooms?.map((roomInfo) => (
|
{currentHierarchy &&
|
||||||
roomInfo.room_id === currentPath.roomId
|
currentHierarchy.rooms?.map((roomInfo) =>
|
||||||
? null
|
roomInfo.room_id === currentPath.roomId ? null : (
|
||||||
: (
|
|
||||||
<SpaceManageItem
|
<SpaceManageItem
|
||||||
key={roomInfo.room_id}
|
key={roomInfo.room_id}
|
||||||
isSelected={selected.includes(roomInfo.room_id)}
|
isSelected={selected.includes(roomInfo.room_id)}
|
||||||
|
@ -361,7 +366,7 @@ function SpaceManageContent({ roomId, requestClose }) {
|
||||||
onSelect={handleSelected}
|
onSelect={handleSelected}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)))}
|
)}
|
||||||
{!currentHierarchy && <Text>loading...</Text>}
|
{!currentHierarchy && <Text>loading...</Text>}
|
||||||
</div>
|
</div>
|
||||||
{currentHierarchy?.canLoadMore && !isLoading && (
|
{currentHierarchy?.canLoadMore && !isLoading && (
|
||||||
|
@ -410,20 +415,16 @@ function SpaceManage() {
|
||||||
<PopupWindow
|
<PopupWindow
|
||||||
isOpen={roomId !== null}
|
isOpen={roomId !== null}
|
||||||
className="space-manage"
|
className="space-manage"
|
||||||
title={(
|
title={
|
||||||
<Text variant="s1" weight="medium" primary>
|
<Text variant="s1" weight="medium" primary>
|
||||||
{roomId && twemojify(room.name)}
|
{roomId && room.name}
|
||||||
<span style={{ color: 'var(--tc-surface-low)' }}> — manage rooms</span>
|
<span style={{ color: 'var(--tc-surface-low)' }}> — manage rooms</span>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
}
|
||||||
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
|
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
|
||||||
onRequestClose={requestClose}
|
onRequestClose={requestClose}
|
||||||
>
|
>
|
||||||
{
|
{roomId ? <SpaceManageContent roomId={roomId} requestClose={requestClose} /> : <div />}
|
||||||
roomId
|
|
||||||
? <SpaceManageContent roomId={roomId} requestClose={requestClose} />
|
|
||||||
: <div />
|
|
||||||
}
|
|
||||||
</PopupWindow>
|
</PopupWindow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,6 @@ import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './SpaceSettings.scss';
|
import './SpaceSettings.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
|
@ -48,23 +46,28 @@ const tabText = {
|
||||||
PERMISSIONS: 'Permissions',
|
PERMISSIONS: 'Permissions',
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabItems = [{
|
const tabItems = [
|
||||||
iconSrc: SettingsIC,
|
{
|
||||||
text: tabText.GENERAL,
|
iconSrc: SettingsIC,
|
||||||
disabled: false,
|
text: tabText.GENERAL,
|
||||||
}, {
|
disabled: false,
|
||||||
iconSrc: UserIC,
|
},
|
||||||
text: tabText.MEMBERS,
|
{
|
||||||
disabled: false,
|
iconSrc: UserIC,
|
||||||
}, {
|
text: tabText.MEMBERS,
|
||||||
iconSrc: EmojiIC,
|
disabled: false,
|
||||||
text: tabText.EMOJIS,
|
},
|
||||||
disabled: false,
|
{
|
||||||
}, {
|
iconSrc: EmojiIC,
|
||||||
iconSrc: ShieldUserIC,
|
text: tabText.EMOJIS,
|
||||||
text: tabText.PERMISSIONS,
|
disabled: false,
|
||||||
disabled: false,
|
},
|
||||||
}];
|
{
|
||||||
|
iconSrc: ShieldUserIC,
|
||||||
|
text: tabText.PERMISSIONS,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
function GeneralSettings({ roomId }) {
|
function GeneralSettings({ roomId }) {
|
||||||
const isPinned = initMatrix.accountData.spaceShortcut.has(roomId);
|
const isPinned = initMatrix.accountData.spaceShortcut.has(roomId);
|
||||||
|
@ -103,7 +106,7 @@ function GeneralSettings({ roomId }) {
|
||||||
'Leave space',
|
'Leave space',
|
||||||
`Are you sure that you want to leave "${roomName}" space?`,
|
`Are you sure that you want to leave "${roomName}" space?`,
|
||||||
'Leave',
|
'Leave',
|
||||||
'danger',
|
'danger'
|
||||||
);
|
);
|
||||||
if (isConfirmed) leave(roomId);
|
if (isConfirmed) leave(roomId);
|
||||||
}}
|
}}
|
||||||
|
@ -165,12 +168,12 @@ function SpaceSettings() {
|
||||||
<PopupWindow
|
<PopupWindow
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
className="space-settings"
|
className="space-settings"
|
||||||
title={(
|
title={
|
||||||
<Text variant="s1" weight="medium" primary>
|
<Text variant="s1" weight="medium" primary>
|
||||||
{isOpen && twemojify(room.name)}
|
{isOpen && room.name}
|
||||||
<span style={{ color: 'var(--tc-surface-low)' }}> — space settings</span>
|
<span style={{ color: 'var(--tc-surface-low)' }}> — space settings</span>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
}
|
||||||
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
|
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
|
||||||
onRequestClose={requestClose}
|
onRequestClose={requestClose}
|
||||||
>
|
>
|
||||||
|
|
Loading…
Reference in a new issue