remove twemoji

This commit is contained in:
array-in-a-matrix 2023-04-04 00:13:32 -04:00
parent 5c5acbd763
commit 6da274d1a9
11 changed files with 412 additions and 382 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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