(chore) remove outdated code (#1765)
* optimize room typing members hook * remove unused code - WIP * remove old code from initMatrix * remove twemojify function * remove old sanitize util * delete old markdown util * delete Math atom component * uninstall unused dependencies * remove old notification system * decrypt message in inbox notification center and fix refresh in background * improve notification --------- Co-authored-by: Krishan <33421343+kfiven@users.noreply.github.com>
This commit is contained in:
@@ -1,35 +0,0 @@
|
||||
class EventLimit {
|
||||
constructor() {
|
||||
this._from = 0;
|
||||
|
||||
this.SMALLEST_EVT_HEIGHT = 32;
|
||||
this.PAGES_COUNT = 4;
|
||||
}
|
||||
|
||||
get maxEvents() {
|
||||
return Math.round(document.body.clientHeight / this.SMALLEST_EVT_HEIGHT) * this.PAGES_COUNT;
|
||||
}
|
||||
|
||||
get from() {
|
||||
return this._from;
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this._from + this.maxEvents;
|
||||
}
|
||||
|
||||
setFrom(from) {
|
||||
this._from = from < 0 ? 0 : from;
|
||||
}
|
||||
|
||||
paginate(backwards, limit, timelineLength) {
|
||||
this._from = backwards ? this._from - limit : this._from + limit;
|
||||
|
||||
if (!backwards && this.length > timelineLength) {
|
||||
this._from = timelineLength - this.maxEvents;
|
||||
}
|
||||
if (this._from < 0) this._from = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export default EventLimit;
|
||||
@@ -1,215 +0,0 @@
|
||||
import React, {
|
||||
useState, useEffect, useCallback, useRef,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './PeopleDrawer.scss';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { getPowerLabel, getUsernameOfRoomMember } from '../../../util/matrixUtil';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { openInviteUser, openProfileViewer } from '../../../client/action/navigation';
|
||||
import AsyncSearch from '../../../util/AsyncSearch';
|
||||
import { memberByAtoZ, memberByPowerLevel } from '../../../util/sort';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Header, { TitleWrapper } from '../../atoms/header/Header';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import SegmentedControl from '../../atoms/segmented-controls/SegmentedControls';
|
||||
import PeopleSelector from '../../molecules/people-selector/PeopleSelector';
|
||||
|
||||
import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
|
||||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||
|
||||
function simplyfiMembers(members) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
return members.map((member) => ({
|
||||
userId: member.userId,
|
||||
name: getUsernameOfRoomMember(member),
|
||||
username: member.userId.slice(1, member.userId.indexOf(':')),
|
||||
avatarSrc: member.getAvatarUrl(mx.baseUrl, 24, 24, 'crop'),
|
||||
peopleRole: getPowerLabel(member.powerLevel),
|
||||
powerLevel: members.powerLevel,
|
||||
}));
|
||||
}
|
||||
|
||||
const asyncSearch = new AsyncSearch();
|
||||
function PeopleDrawer({ roomId }) {
|
||||
const PER_PAGE_MEMBER = 50;
|
||||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(roomId);
|
||||
const canInvite = room?.canInvite(mx.getUserId());
|
||||
|
||||
const [itemCount, setItemCount] = useState(PER_PAGE_MEMBER);
|
||||
const [membership, setMembership] = useState('join');
|
||||
const [memberList, setMemberList] = useState([]);
|
||||
const [searchedMembers, setSearchedMembers] = useState(null);
|
||||
const searchRef = useRef(null);
|
||||
|
||||
const getMembersWithMembership = useCallback(
|
||||
(mship) => room.getMembersWithMembership(mship),
|
||||
[roomId, membership],
|
||||
);
|
||||
|
||||
function loadMorePeople() {
|
||||
setItemCount(itemCount + PER_PAGE_MEMBER);
|
||||
}
|
||||
|
||||
function handleSearchData(data) {
|
||||
// NOTICE: data is passed as object property
|
||||
// because react sucks at handling state update with array.
|
||||
setSearchedMembers({ data });
|
||||
setItemCount(PER_PAGE_MEMBER);
|
||||
}
|
||||
|
||||
function handleSearch(e) {
|
||||
const term = e.target.value;
|
||||
if (term === '' || term === undefined) {
|
||||
searchRef.current.value = '';
|
||||
searchRef.current.focus();
|
||||
setSearchedMembers(null);
|
||||
setItemCount(PER_PAGE_MEMBER);
|
||||
} else asyncSearch.search(term);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
asyncSearch.setup(memberList, {
|
||||
keys: ['name', 'username', 'userId'],
|
||||
limit: PER_PAGE_MEMBER,
|
||||
});
|
||||
}, [memberList]);
|
||||
|
||||
useEffect(() => {
|
||||
let isLoadingMembers = false;
|
||||
let isRoomChanged = false;
|
||||
const updateMemberList = (event) => {
|
||||
if (isLoadingMembers) return;
|
||||
if (event && event?.getRoomId() !== roomId) return;
|
||||
setMemberList(
|
||||
simplyfiMembers(
|
||||
getMembersWithMembership(membership)
|
||||
.sort(memberByAtoZ).sort(memberByPowerLevel),
|
||||
),
|
||||
);
|
||||
};
|
||||
searchRef.current.value = '';
|
||||
updateMemberList();
|
||||
isLoadingMembers = true;
|
||||
room.loadMembersIfNeeded().then(() => {
|
||||
isLoadingMembers = false;
|
||||
if (isRoomChanged) return;
|
||||
updateMemberList();
|
||||
});
|
||||
|
||||
asyncSearch.on(asyncSearch.RESULT_SENT, handleSearchData);
|
||||
mx.on('RoomMember.membership', updateMemberList);
|
||||
mx.on('RoomMember.powerLevel', updateMemberList);
|
||||
return () => {
|
||||
isRoomChanged = true;
|
||||
setMemberList([]);
|
||||
setSearchedMembers(null);
|
||||
setItemCount(PER_PAGE_MEMBER);
|
||||
asyncSearch.removeListener(asyncSearch.RESULT_SENT, handleSearchData);
|
||||
mx.removeListener('RoomMember.membership', updateMemberList);
|
||||
mx.removeListener('RoomMember.powerLevel', updateMemberList);
|
||||
};
|
||||
}, [roomId, membership]);
|
||||
|
||||
useEffect(() => {
|
||||
setMembership('join');
|
||||
}, [roomId]);
|
||||
|
||||
const mList = searchedMembers !== null ? searchedMembers.data : memberList.slice(0, itemCount);
|
||||
return (
|
||||
<div className="people-drawer">
|
||||
<Header>
|
||||
<TitleWrapper>
|
||||
<Text variant="s1" primary>
|
||||
People
|
||||
<Text className="people-drawer__member-count" variant="b3">{`${room.getJoinedMemberCount()} members`}</Text>
|
||||
</Text>
|
||||
</TitleWrapper>
|
||||
<IconButton onClick={() => openInviteUser(roomId)} tooltip="Invite" src={AddUserIC} disabled={!canInvite} />
|
||||
</Header>
|
||||
<div className="people-drawer__content-wrapper">
|
||||
<div className="people-drawer__scrollable">
|
||||
<ScrollView autoHide>
|
||||
<div className="people-drawer__content">
|
||||
<SegmentedControl
|
||||
selected={
|
||||
(() => {
|
||||
const getSegmentIndex = {
|
||||
join: 0,
|
||||
invite: 1,
|
||||
ban: 2,
|
||||
};
|
||||
return getSegmentIndex[membership];
|
||||
})()
|
||||
}
|
||||
segments={[{ text: 'Joined' }, { text: 'Invited' }, { text: 'Banned' }]}
|
||||
onSelect={(index) => {
|
||||
const selectSegment = [
|
||||
() => setMembership('join'),
|
||||
() => setMembership('invite'),
|
||||
() => setMembership('ban'),
|
||||
];
|
||||
selectSegment[index]?.();
|
||||
}}
|
||||
/>
|
||||
{
|
||||
mList.map((member) => (
|
||||
<PeopleSelector
|
||||
key={member.userId}
|
||||
onClick={() => openProfileViewer(member.userId, roomId)}
|
||||
avatarSrc={member.avatarSrc}
|
||||
name={member.name}
|
||||
color={colorMXID(member.userId)}
|
||||
peopleRole={member.peopleRole}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{
|
||||
(searchedMembers?.data.length === 0 || memberList.length === 0)
|
||||
&& (
|
||||
<div className="people-drawer__noresult">
|
||||
<Text variant="b2">No results found!</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className="people-drawer__load-more">
|
||||
{
|
||||
mList.length !== 0
|
||||
&& memberList.length > itemCount
|
||||
&& searchedMembers === null
|
||||
&& (
|
||||
<Button onClick={loadMorePeople}>View more</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollView>
|
||||
</div>
|
||||
<div className="people-drawer__sticky">
|
||||
<form onSubmit={(e) => e.preventDefault()} className="people-search">
|
||||
<RawIcon size="small" src={SearchIC} />
|
||||
<Input forwardRef={searchRef} type="text" onChange={handleSearch} placeholder="Search" required />
|
||||
{
|
||||
searchedMembers !== null
|
||||
&& <IconButton onClick={handleSearch} size="small" src={CrossIC} />
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
PeopleDrawer.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default PeopleDrawer;
|
||||
@@ -1,93 +0,0 @@
|
||||
@use '../../partials/flex';
|
||||
@use '../../partials/dir';
|
||||
|
||||
.people-drawer {
|
||||
@extend .cp-fx__column;
|
||||
width: var(--people-drawer-width);
|
||||
background-color: var(--bg-surface-low);
|
||||
@include dir.side(border, 1px solid var(--bg-surface-border), none);
|
||||
|
||||
&__member-count {
|
||||
color: var(--tc-surface-low);
|
||||
}
|
||||
|
||||
&__content-wrapper {
|
||||
@extend .cp-fx__item-one;
|
||||
@extend .cp-fx__column;
|
||||
}
|
||||
|
||||
&__scrollable {
|
||||
@extend .cp-fx__item-one;
|
||||
}
|
||||
|
||||
&__noresult {
|
||||
padding: var(--sp-extra-tight) var(--sp-normal);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__sticky {
|
||||
& .people-search {
|
||||
--search-input-height: 40px;
|
||||
min-height: var(--search-input-height);
|
||||
|
||||
margin: 0 var(--sp-extra-tight);
|
||||
|
||||
position: relative;
|
||||
bottom: var(--sp-normal);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& > .ic-raw,
|
||||
& > .ic-btn {
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
}
|
||||
& > .ic-raw {
|
||||
@include dir.prop(left, var(--sp-tight), unset);
|
||||
@include dir.prop(right, unset, var(--sp-tight));
|
||||
}
|
||||
& > .ic-btn {
|
||||
@include dir.prop(right, 2px, unset);
|
||||
@include dir.prop(left, unset, 2px);
|
||||
}
|
||||
& .input-container {
|
||||
flex: 1;
|
||||
}
|
||||
& .input {
|
||||
padding: 0 44px;
|
||||
height: var(--search-input-height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.people-drawer__content {
|
||||
padding-top: var(--sp-extra-tight);
|
||||
padding-bottom: calc(2 * var(--sp-normal));
|
||||
|
||||
& .people-selector {
|
||||
padding: var(--sp-extra-tight);
|
||||
border-radius: var(--bo-radius);
|
||||
&__container {
|
||||
@include dir.side(margin, var(--sp-extra-tight), 0);
|
||||
}
|
||||
}
|
||||
|
||||
& .segmented-controls {
|
||||
display: flex;
|
||||
margin-bottom: var(--sp-extra-tight);
|
||||
@include dir.side(margin, var(--sp-extra-tight), 0);
|
||||
}
|
||||
& .segment-btn {
|
||||
flex: 1;
|
||||
padding: var(--sp-ultra-tight) 0;
|
||||
}
|
||||
}
|
||||
.people-drawer__load-more {
|
||||
padding: var(--sp-normal) 0 0;
|
||||
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
|
||||
|
||||
& .btn-surface {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
@use '../../partials/flex';
|
||||
@use '../../partials/screen';
|
||||
|
||||
.room {
|
||||
@extend .cp-fx__row;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
|
||||
&__content {
|
||||
@extend .cp-fx__item-one;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.room .people-drawer {
|
||||
@include screen.smallerThan(tabletBreakpoint) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import './RoomSettings.scss';
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import * as roomActions from '../../../client/action/room';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Tabs from '../../atoms/tabs/Tabs';
|
||||
@@ -86,7 +85,7 @@ function GeneralSettings({ roomId }) {
|
||||
'danger'
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
roomActions.leave(roomId);
|
||||
mx.leave(roomId);
|
||||
}}
|
||||
iconSrc={LeaveArrowIC}
|
||||
>
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
@use '../../partials/flex';
|
||||
@use '../../partials/screen';
|
||||
@use '../../partials/dir';
|
||||
|
||||
.room-view {
|
||||
@extend .cp-fx__column;
|
||||
background-color: var(--bg-surface);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
box-shadow: none;
|
||||
|
||||
transition: transform 200ms var(--fluid-slide-down);
|
||||
|
||||
&--dropped {
|
||||
transform: translateY(calc(100% - var(--header-height)));
|
||||
border-radius: var(--bo-radius) var(--bo-radius) 0 0;
|
||||
box-shadow: var(--bs-popup);
|
||||
}
|
||||
|
||||
& .header {
|
||||
@include screen.smallerThan(mobileBreakpoint) {
|
||||
padding: 0 var(--sp-tight);
|
||||
}
|
||||
}
|
||||
|
||||
&__content-wrapper {
|
||||
@extend .cp-fx__item-one;
|
||||
@extend .cp-fx__column;
|
||||
}
|
||||
|
||||
&__scrollable {
|
||||
@extend .cp-fx__item-one;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__sticky {
|
||||
position: relative;
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
&__editor {
|
||||
padding: 0 var(--sp-normal);
|
||||
}
|
||||
}
|
||||
@@ -1,297 +0,0 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomViewCmdBar.scss';
|
||||
import parse from 'html-react-parser';
|
||||
import twemoji from 'twemoji';
|
||||
|
||||
import { twemojify, TWEMOJI_BASE_URL } from '../../../util/twemojify';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { getEmojiForCompletion } from '../emoji-board/custom-emoji';
|
||||
import AsyncSearch from '../../../util/AsyncSearch';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||
import FollowingMembers from '../../molecules/following-members/FollowingMembers';
|
||||
import { addRecentEmoji, getRecentEmojis } from '../emoji-board/recent';
|
||||
import commands from './commands';
|
||||
|
||||
function CmdItem({ onClick, children }) {
|
||||
return (
|
||||
<button className="cmd-item" onClick={onClick} type="button">
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
CmdItem.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
|
||||
function renderCmdSuggestions(cmdPrefix, cmds) {
|
||||
const cmdOptString = typeof option === 'string' ? `/${option}` : '/?';
|
||||
return cmds.map((cmd) => (
|
||||
<CmdItem
|
||||
key={cmd}
|
||||
onClick={() => {
|
||||
fireCmd({
|
||||
prefix: cmdPrefix,
|
||||
option,
|
||||
result: commands[cmd],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text variant="b2">{`${cmd}${cmd.isOptions ? cmdOptString : ''}`}</Text>
|
||||
</CmdItem>
|
||||
));
|
||||
}
|
||||
|
||||
function renderEmojiSuggestion(emPrefix, emos) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
|
||||
// Renders a small Twemoji
|
||||
function renderTwemoji(emoji) {
|
||||
return parse(
|
||||
twemoji.parse(emoji.unicode, {
|
||||
attributes: () => ({
|
||||
unicode: emoji.unicode,
|
||||
shortcodes: emoji.shortcodes?.toString(),
|
||||
}),
|
||||
base: TWEMOJI_BASE_URL,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Render a custom emoji
|
||||
function renderCustomEmoji(emoji) {
|
||||
return (
|
||||
<img
|
||||
className="emoji"
|
||||
src={mx.mxcUrlToHttp(emoji.mxc)}
|
||||
data-mx-emoticon=""
|
||||
alt={`:${emoji.shortcode}:`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Dynamically render either a custom emoji or twemoji based on what the input is
|
||||
function renderEmoji(emoji) {
|
||||
if (emoji.mxc) {
|
||||
return renderCustomEmoji(emoji);
|
||||
}
|
||||
return renderTwemoji(emoji);
|
||||
}
|
||||
|
||||
return emos.map((emoji) => (
|
||||
<CmdItem
|
||||
key={emoji.shortcode}
|
||||
onClick={() =>
|
||||
fireCmd({
|
||||
prefix: emPrefix,
|
||||
result: emoji,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Text variant="b1">{renderEmoji(emoji)}</Text>
|
||||
<Text variant="b2">{`:${emoji.shortcode}:`}</Text>
|
||||
</CmdItem>
|
||||
));
|
||||
}
|
||||
|
||||
function renderNameSuggestion(namePrefix, members) {
|
||||
return members.map((member) => (
|
||||
<CmdItem
|
||||
key={member.userId}
|
||||
onClick={() => {
|
||||
fireCmd({
|
||||
prefix: namePrefix,
|
||||
result: member,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text variant="b2">{twemojify(member.name)}</Text>
|
||||
</CmdItem>
|
||||
));
|
||||
}
|
||||
|
||||
const cmd = {
|
||||
'/': (cmds) => renderCmdSuggestions(prefix, cmds),
|
||||
':': (emos) => renderEmojiSuggestion(prefix, emos),
|
||||
'@': (members) => renderNameSuggestion(prefix, members),
|
||||
};
|
||||
return cmd[prefix]?.(suggestions);
|
||||
}
|
||||
|
||||
const asyncSearch = new AsyncSearch();
|
||||
let cmdPrefix;
|
||||
let cmdOption;
|
||||
function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
|
||||
const [cmd, setCmd] = useState(null);
|
||||
|
||||
function displaySuggestions(suggestions) {
|
||||
if (suggestions.length === 0) {
|
||||
setCmd({ prefix: cmd?.prefix || cmdPrefix, error: 'No suggestion found.' });
|
||||
viewEvent.emit('cmd_error');
|
||||
return;
|
||||
}
|
||||
setCmd({ prefix: cmd?.prefix || cmdPrefix, suggestions, option: cmdOption });
|
||||
}
|
||||
|
||||
function processCmd(prefix, slug) {
|
||||
let searchTerm = slug;
|
||||
cmdOption = undefined;
|
||||
cmdPrefix = prefix;
|
||||
if (prefix === '/') {
|
||||
const cmdSlugParts = slug.split('/');
|
||||
[searchTerm, cmdOption] = cmdSlugParts;
|
||||
}
|
||||
if (prefix === ':') {
|
||||
if (searchTerm.length <= 3) {
|
||||
if (searchTerm.match(/^[-]?(\))$/)) searchTerm = 'smile';
|
||||
else if (searchTerm.match(/^[-]?(s|S)$/)) searchTerm = 'confused';
|
||||
else if (searchTerm.match(/^[-]?(o|O|0)$/)) searchTerm = 'astonished';
|
||||
else if (searchTerm.match(/^[-]?(\|)$/)) searchTerm = 'neutral_face';
|
||||
else if (searchTerm.match(/^[-]?(d|D)$/)) searchTerm = 'grin';
|
||||
else if (searchTerm.match(/^[-]?(\/)$/)) searchTerm = 'frown';
|
||||
else if (searchTerm.match(/^[-]?(p|P)$/)) searchTerm = 'stuck_out_tongue';
|
||||
else if (searchTerm.match(/^'[-]?(\()$/)) searchTerm = 'cry';
|
||||
else if (searchTerm.match(/^[-]?(x|X)$/)) searchTerm = 'dizzy_face';
|
||||
else if (searchTerm.match(/^[-]?(\()$/)) searchTerm = 'pleading_face';
|
||||
else if (searchTerm.match(/^[-]?(\$)$/)) searchTerm = 'money';
|
||||
else if (searchTerm.match(/^(<3)$/)) searchTerm = 'heart';
|
||||
else if (searchTerm.match(/^(c|ca|cat)$/)) searchTerm = '_cat';
|
||||
}
|
||||
}
|
||||
|
||||
asyncSearch.search(searchTerm);
|
||||
}
|
||||
function activateCmd(prefix) {
|
||||
cmdPrefix = prefix;
|
||||
cmdPrefix = undefined;
|
||||
|
||||
const mx = initMatrix.matrixClient;
|
||||
const setupSearch = {
|
||||
'/': () => {
|
||||
asyncSearch.setup(Object.keys(commands), { isContain: true });
|
||||
setCmd({ prefix, suggestions: Object.keys(commands) });
|
||||
},
|
||||
':': () => {
|
||||
const parentIds = initMatrix.roomList.getAllParentSpaces(roomId);
|
||||
const parentRooms = [...parentIds].map((id) => mx.getRoom(id));
|
||||
const emojis = getEmojiForCompletion(mx, [mx.getRoom(roomId), ...parentRooms]);
|
||||
const recentEmoji = getRecentEmojis(20);
|
||||
asyncSearch.setup(emojis, { keys: ['shortcode'], isContain: true, limit: 20 });
|
||||
setCmd({
|
||||
prefix,
|
||||
suggestions: recentEmoji.length > 0 ? recentEmoji : emojis.slice(26, 46),
|
||||
});
|
||||
},
|
||||
'@': () => {
|
||||
const members = mx
|
||||
.getRoom(roomId)
|
||||
.getJoinedMembers()
|
||||
.map((member) => ({
|
||||
name: member.name,
|
||||
userId: member.userId.slice(1),
|
||||
}));
|
||||
asyncSearch.setup(members, { keys: ['name', 'userId'], limit: 20 });
|
||||
const endIndex = members.length > 20 ? 20 : members.length;
|
||||
setCmd({ prefix, suggestions: members.slice(0, endIndex) });
|
||||
},
|
||||
};
|
||||
setupSearch[prefix]?.();
|
||||
}
|
||||
function deactivateCmd() {
|
||||
setCmd(null);
|
||||
cmdOption = undefined;
|
||||
cmdPrefix = undefined;
|
||||
}
|
||||
function fireCmd(myCmd) {
|
||||
if (myCmd.prefix === '/') {
|
||||
viewEvent.emit('cmd_fired', {
|
||||
replace: `/${myCmd.result.name}`,
|
||||
});
|
||||
}
|
||||
if (myCmd.prefix === ':') {
|
||||
if (!myCmd.result.mxc) addRecentEmoji(myCmd.result.unicode);
|
||||
viewEvent.emit('cmd_fired', {
|
||||
replace: myCmd.result.mxc ? `:${myCmd.result.shortcode}: ` : myCmd.result.unicode,
|
||||
});
|
||||
}
|
||||
if (myCmd.prefix === '@') {
|
||||
viewEvent.emit('cmd_fired', {
|
||||
replace: `@${myCmd.result.userId}`,
|
||||
});
|
||||
}
|
||||
deactivateCmd();
|
||||
}
|
||||
|
||||
function listenKeyboard(event) {
|
||||
const { activeElement } = document;
|
||||
const lastCmdItem = document.activeElement.parentNode.lastElementChild;
|
||||
if (event.key === 'Escape') {
|
||||
if (activeElement.className !== 'cmd-item') return;
|
||||
viewEvent.emit('focus_msg_input');
|
||||
}
|
||||
if (event.key === 'Tab') {
|
||||
if (lastCmdItem.className !== 'cmd-item') return;
|
||||
if (lastCmdItem !== activeElement) return;
|
||||
if (event.shiftKey) return;
|
||||
viewEvent.emit('focus_msg_input');
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
viewEvent.on('cmd_activate', activateCmd);
|
||||
viewEvent.on('cmd_deactivate', deactivateCmd);
|
||||
return () => {
|
||||
deactivateCmd();
|
||||
viewEvent.removeListener('cmd_activate', activateCmd);
|
||||
viewEvent.removeListener('cmd_deactivate', deactivateCmd);
|
||||
};
|
||||
}, [roomId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cmd !== null) document.body.addEventListener('keydown', listenKeyboard);
|
||||
viewEvent.on('cmd_process', processCmd);
|
||||
asyncSearch.on(asyncSearch.RESULT_SENT, displaySuggestions);
|
||||
return () => {
|
||||
if (cmd !== null) document.body.removeEventListener('keydown', listenKeyboard);
|
||||
|
||||
viewEvent.removeListener('cmd_process', processCmd);
|
||||
asyncSearch.removeListener(asyncSearch.RESULT_SENT, displaySuggestions);
|
||||
};
|
||||
}, [cmd]);
|
||||
|
||||
const isError = typeof cmd?.error === 'string';
|
||||
if (cmd === null || isError) {
|
||||
return (
|
||||
<div className="cmd-bar">
|
||||
<FollowingMembers roomTimeline={roomTimeline} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="cmd-bar">
|
||||
<div className="cmd-bar__info">
|
||||
<Text variant="b3">TAB</Text>
|
||||
</div>
|
||||
<div className="cmd-bar__content">
|
||||
<ScrollView horizontal vertical={false} invisible>
|
||||
<div className="cmd-bar__content-suggestions">{renderSuggestions(cmd, fireCmd)}</div>
|
||||
</ScrollView>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
RoomViewCmdBar.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
roomTimeline: PropTypes.shape({}).isRequired,
|
||||
viewEvent: PropTypes.shape({}).isRequired,
|
||||
};
|
||||
|
||||
export default RoomViewCmdBar;
|
||||
@@ -1,57 +0,0 @@
|
||||
@use '../../partials/flex';
|
||||
@use '../../partials/text';
|
||||
@use '../../partials/dir';
|
||||
|
||||
.cmd-bar {
|
||||
--cmd-bar-height: 28px;
|
||||
min-height: var(--cmd-bar-height);
|
||||
display: flex;
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
width: 40px;
|
||||
@include dir.side(margin, 14px, 10px);
|
||||
|
||||
& > * {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
@extend .cp-fx__item-one;
|
||||
display: flex;
|
||||
|
||||
&-suggestions {
|
||||
height: 100%;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& > .text {
|
||||
@extend .cp-txt__ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cmd-item {
|
||||
--cmd-item-bar: inset 0 -2px 0 0 var(--bg-caution);
|
||||
height: 100%;
|
||||
@include dir.side(margin, 0, var(--sp-extra-tight));
|
||||
padding: 0 var(--sp-extra-tight);
|
||||
border-radius: var(--bo-radius) var(--bo-radius) 0 0;
|
||||
cursor: pointer;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-caution-hover);
|
||||
}
|
||||
&:focus {
|
||||
background-color: var(--bg-caution-active);
|
||||
box-shadow: var(--cmd-item-bar);
|
||||
border-bottom: 2px solid transparent;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
@@ -1,644 +0,0 @@
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, {
|
||||
useState, useEffect, useLayoutEffect, useCallback, useRef,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomViewContent.scss';
|
||||
|
||||
import dateFormat from 'dateformat';
|
||||
import { twemojify } from '../../../util/twemojify';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import { openProfileViewer } from '../../../client/action/navigation';
|
||||
import { diffMinutes, isInSameDay, Throttle } from '../../../util/common';
|
||||
import { markAsRead } from '../../../client/action/notifications';
|
||||
|
||||
import Divider from '../../atoms/divider/Divider';
|
||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||
import { Message, PlaceholderMessage } from '../../molecules/message/Message';
|
||||
import RoomIntro from '../../molecules/room-intro/RoomIntro';
|
||||
import TimelineChange from '../../molecules/message/TimelineChange';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||
import { parseTimelineChange } from './common';
|
||||
import TimelineScroll from './TimelineScroll';
|
||||
import EventLimit from './EventLimit';
|
||||
import { getResizeObserverEntry, useResizeObserver } from '../../hooks/useResizeObserver';
|
||||
|
||||
const PAG_LIMIT = 30;
|
||||
const MAX_MSG_DIFF_MINUTES = 5;
|
||||
const PLACEHOLDER_COUNT = 2;
|
||||
const PLACEHOLDERS_HEIGHT = 96 * PLACEHOLDER_COUNT;
|
||||
const SCROLL_TRIGGER_POS = PLACEHOLDERS_HEIGHT * 4;
|
||||
|
||||
function loadingMsgPlaceholders(key, count = 2) {
|
||||
const pl = [];
|
||||
const genPlaceholders = () => {
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
pl.push(<PlaceholderMessage key={`placeholder-${i}${key}`} />);
|
||||
}
|
||||
return pl;
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment key={`placeholder-container${key}`}>
|
||||
{genPlaceholders()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function RoomIntroContainer({ event, timeline }) {
|
||||
const [, nameForceUpdate] = useForceUpdate();
|
||||
const mx = initMatrix.matrixClient;
|
||||
const { roomList } = initMatrix;
|
||||
const { room } = timeline;
|
||||
const roomTopic = room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
|
||||
const isDM = roomList.directs.has(timeline.roomId);
|
||||
let avatarSrc = room.getAvatarUrl(mx.baseUrl, 80, 80, 'crop');
|
||||
avatarSrc = isDM ? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 80, 80, 'crop') : avatarSrc;
|
||||
|
||||
const heading = isDM ? room.name : `Welcome to ${room.name}`;
|
||||
const topic = twemojify(roomTopic || '', undefined, true);
|
||||
const nameJsx = twemojify(room.name);
|
||||
const desc = isDM
|
||||
? (
|
||||
<>
|
||||
This is the beginning of your direct message history with @
|
||||
<b>{nameJsx}</b>
|
||||
{'. '}
|
||||
{topic}
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
{'This is the beginning of the '}
|
||||
<b>{nameJsx}</b>
|
||||
{' room. '}
|
||||
{topic}
|
||||
</>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleUpdate = () => nameForceUpdate();
|
||||
|
||||
roomList.on(cons.events.roomList.ROOM_PROFILE_UPDATED, handleUpdate);
|
||||
return () => {
|
||||
roomList.removeListener(cons.events.roomList.ROOM_PROFILE_UPDATED, handleUpdate);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<RoomIntro
|
||||
roomId={timeline.roomId}
|
||||
avatarSrc={avatarSrc}
|
||||
name={room.name}
|
||||
heading={twemojify(heading)}
|
||||
desc={desc}
|
||||
time={event ? `Created at ${dateFormat(event.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function handleOnClickCapture(e) {
|
||||
const { target, nativeEvent } = e;
|
||||
|
||||
const userId = target.getAttribute('data-mx-pill');
|
||||
if (userId) {
|
||||
const roomId = navigation.selectedRoomId;
|
||||
openProfileViewer(userId, roomId);
|
||||
}
|
||||
|
||||
const spoiler = nativeEvent.composedPath().find((el) => el?.hasAttribute?.('data-mx-spoiler'));
|
||||
if (spoiler) {
|
||||
if (!spoiler.classList.contains('data-mx-spoiler--visible')) e.preventDefault();
|
||||
spoiler.classList.toggle('data-mx-spoiler--visible');
|
||||
}
|
||||
}
|
||||
|
||||
function renderEvent(
|
||||
roomTimeline,
|
||||
mEvent,
|
||||
prevMEvent,
|
||||
isFocus,
|
||||
isEdit,
|
||||
setEdit,
|
||||
cancelEdit,
|
||||
) {
|
||||
const isBodyOnly = (prevMEvent !== null
|
||||
&& prevMEvent.getSender() === mEvent.getSender()
|
||||
&& prevMEvent.getType() !== 'm.room.member'
|
||||
&& prevMEvent.getType() !== 'm.room.create'
|
||||
&& diffMinutes(mEvent.getDate(), prevMEvent.getDate()) <= MAX_MSG_DIFF_MINUTES
|
||||
);
|
||||
const timestamp = mEvent.getTs();
|
||||
|
||||
if (mEvent.getType() === 'm.room.member') {
|
||||
const timelineChange = parseTimelineChange(mEvent);
|
||||
if (timelineChange === null) return <div key={mEvent.getId()} />;
|
||||
return (
|
||||
<TimelineChange
|
||||
key={mEvent.getId()}
|
||||
variant={timelineChange.variant}
|
||||
content={timelineChange.content}
|
||||
timestamp={timestamp}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Message
|
||||
key={mEvent.getId()}
|
||||
mEvent={mEvent}
|
||||
isBodyOnly={isBodyOnly}
|
||||
roomTimeline={roomTimeline}
|
||||
focus={isFocus}
|
||||
fullTime={false}
|
||||
isEdit={isEdit}
|
||||
setEdit={setEdit}
|
||||
cancelEdit={cancelEdit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function useTimeline(roomTimeline, eventId, readUptoEvtStore, eventLimitRef) {
|
||||
const [timelineInfo, setTimelineInfo] = useState(null);
|
||||
|
||||
const setEventTimeline = async (eId) => {
|
||||
if (typeof eId === 'string') {
|
||||
const isLoaded = await roomTimeline.loadEventTimeline(eId);
|
||||
if (isLoaded) return;
|
||||
// if eventTimeline failed to load,
|
||||
// we will load live timeline as fallback.
|
||||
}
|
||||
roomTimeline.loadLiveTimeline();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const limit = eventLimitRef.current;
|
||||
const initTimeline = (eId) => {
|
||||
// NOTICE: eId can be id of readUpto, reply or specific event.
|
||||
// readUpTo: when user click jump to unread message button.
|
||||
// reply: when user click reply from timeline.
|
||||
// specific event when user open a link of event. behave same as ^^^^
|
||||
const readUpToId = roomTimeline.getReadUpToEventId();
|
||||
let focusEventIndex = -1;
|
||||
const isSpecificEvent = eId && eId !== readUpToId;
|
||||
|
||||
if (isSpecificEvent) {
|
||||
focusEventIndex = roomTimeline.getEventIndex(eId);
|
||||
}
|
||||
if (!readUptoEvtStore.getItem() && roomTimeline.hasEventInTimeline(readUpToId)) {
|
||||
// either opening live timeline or jump to unread.
|
||||
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
|
||||
}
|
||||
if (readUptoEvtStore.getItem() && !isSpecificEvent) {
|
||||
focusEventIndex = roomTimeline.getUnreadEventIndex(readUptoEvtStore.getItem().getId());
|
||||
}
|
||||
|
||||
if (focusEventIndex > -1) {
|
||||
limit.setFrom(focusEventIndex - Math.round(limit.maxEvents / 2));
|
||||
} else {
|
||||
limit.setFrom(roomTimeline.timeline.length - limit.maxEvents);
|
||||
}
|
||||
setTimelineInfo({ focusEventId: isSpecificEvent ? eId : null });
|
||||
};
|
||||
|
||||
roomTimeline.on(cons.events.roomTimeline.READY, initTimeline);
|
||||
setEventTimeline(eventId);
|
||||
return () => {
|
||||
roomTimeline.removeListener(cons.events.roomTimeline.READY, initTimeline);
|
||||
limit.setFrom(0);
|
||||
};
|
||||
}, [roomTimeline, eventId]);
|
||||
|
||||
return timelineInfo;
|
||||
}
|
||||
|
||||
function usePaginate(
|
||||
roomTimeline,
|
||||
readUptoEvtStore,
|
||||
forceUpdateLimit,
|
||||
timelineScrollRef,
|
||||
eventLimitRef,
|
||||
) {
|
||||
const [info, setInfo] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePaginatedFromServer = (backwards, loaded) => {
|
||||
const limit = eventLimitRef.current;
|
||||
if (loaded === 0) return;
|
||||
if (!readUptoEvtStore.getItem()) {
|
||||
const readUpToId = roomTimeline.getReadUpToEventId();
|
||||
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
|
||||
}
|
||||
limit.paginate(backwards, PAG_LIMIT, roomTimeline.timeline.length);
|
||||
setTimeout(() => setInfo({
|
||||
backwards,
|
||||
loaded,
|
||||
}));
|
||||
};
|
||||
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
|
||||
return () => {
|
||||
roomTimeline.removeListener(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
|
||||
};
|
||||
}, [roomTimeline]);
|
||||
|
||||
const autoPaginate = useCallback(async () => {
|
||||
const timelineScroll = timelineScrollRef.current;
|
||||
const limit = eventLimitRef.current;
|
||||
if (roomTimeline.isOngoingPagination) return;
|
||||
const tLength = roomTimeline.timeline.length;
|
||||
|
||||
if (timelineScroll.bottom < SCROLL_TRIGGER_POS) {
|
||||
if (limit.length < tLength) {
|
||||
// paginate from memory
|
||||
limit.paginate(false, PAG_LIMIT, tLength);
|
||||
forceUpdateLimit();
|
||||
} else if (roomTimeline.canPaginateForward()) {
|
||||
// paginate from server.
|
||||
await roomTimeline.paginateTimeline(false, PAG_LIMIT);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (timelineScroll.top < SCROLL_TRIGGER_POS) {
|
||||
if (limit.from > 0) {
|
||||
// paginate from memory
|
||||
limit.paginate(true, PAG_LIMIT, tLength);
|
||||
forceUpdateLimit();
|
||||
} else if (roomTimeline.canPaginateBackward()) {
|
||||
// paginate from server.
|
||||
await roomTimeline.paginateTimeline(true, PAG_LIMIT);
|
||||
}
|
||||
}
|
||||
}, [roomTimeline]);
|
||||
|
||||
return [info, autoPaginate];
|
||||
}
|
||||
|
||||
function useHandleScroll(
|
||||
roomTimeline,
|
||||
autoPaginate,
|
||||
readUptoEvtStore,
|
||||
forceUpdateLimit,
|
||||
timelineScrollRef,
|
||||
eventLimitRef,
|
||||
) {
|
||||
const handleScroll = useCallback(() => {
|
||||
const timelineScroll = timelineScrollRef.current;
|
||||
const limit = eventLimitRef.current;
|
||||
requestAnimationFrame(() => {
|
||||
// emit event to toggle scrollToBottom button visibility
|
||||
const isAtBottom = (
|
||||
timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()
|
||||
&& limit.length >= roomTimeline.timeline.length
|
||||
);
|
||||
roomTimeline.emit(cons.events.roomTimeline.AT_BOTTOM, isAtBottom);
|
||||
if (isAtBottom && readUptoEvtStore.getItem()) {
|
||||
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
|
||||
}
|
||||
});
|
||||
autoPaginate();
|
||||
}, [roomTimeline]);
|
||||
|
||||
const handleScrollToLive = useCallback(() => {
|
||||
const timelineScroll = timelineScrollRef.current;
|
||||
const limit = eventLimitRef.current;
|
||||
if (readUptoEvtStore.getItem()) {
|
||||
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
|
||||
}
|
||||
if (roomTimeline.isServingLiveTimeline()) {
|
||||
limit.setFrom(roomTimeline.timeline.length - limit.maxEvents);
|
||||
timelineScroll.scrollToBottom();
|
||||
forceUpdateLimit();
|
||||
return;
|
||||
}
|
||||
roomTimeline.loadLiveTimeline();
|
||||
}, [roomTimeline]);
|
||||
|
||||
return [handleScroll, handleScrollToLive];
|
||||
}
|
||||
|
||||
function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, eventLimitRef) {
|
||||
const myUserId = initMatrix.matrixClient.getUserId();
|
||||
const [newEvent, setEvent] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const timelineScroll = timelineScrollRef.current;
|
||||
const limit = eventLimitRef.current;
|
||||
const trySendReadReceipt = (event) => {
|
||||
if (myUserId === event.getSender()) {
|
||||
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
|
||||
return;
|
||||
}
|
||||
const readUpToEvent = readUptoEvtStore.getItem();
|
||||
const readUpToId = roomTimeline.getReadUpToEventId();
|
||||
const isUnread = readUpToEvent ? readUpToEvent?.getId() === readUpToId : true;
|
||||
|
||||
if (isUnread === false) {
|
||||
if (document.visibilityState === 'visible' && timelineScroll.bottom < 16) {
|
||||
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
|
||||
} else {
|
||||
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { timeline } = roomTimeline;
|
||||
const unreadMsgIsLast = timeline[timeline.length - 2].getId() === readUpToId;
|
||||
if (unreadMsgIsLast) {
|
||||
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
|
||||
}
|
||||
};
|
||||
|
||||
const handleEvent = (event) => {
|
||||
const tLength = roomTimeline.timeline.length;
|
||||
const isViewingLive = roomTimeline.isServingLiveTimeline() && limit.length >= tLength - 1;
|
||||
const isAttached = timelineScroll.bottom < SCROLL_TRIGGER_POS;
|
||||
|
||||
if (isViewingLive && isAttached && document.hasFocus()) {
|
||||
limit.setFrom(tLength - limit.maxEvents);
|
||||
trySendReadReceipt(event);
|
||||
setEvent(event);
|
||||
return;
|
||||
}
|
||||
const isRelates = (event.getType() === 'm.reaction' || event.getRelation()?.rel_type === 'm.replace');
|
||||
if (isRelates) {
|
||||
setEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isViewingLive) {
|
||||
// This stateUpdate will help to put the
|
||||
// loading msg placeholder at bottom
|
||||
setEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEventRedact = (event) => setEvent(event);
|
||||
|
||||
roomTimeline.on(cons.events.roomTimeline.EVENT, handleEvent);
|
||||
roomTimeline.on(cons.events.roomTimeline.EVENT_REDACTED, handleEventRedact);
|
||||
return () => {
|
||||
roomTimeline.removeListener(cons.events.roomTimeline.EVENT, handleEvent);
|
||||
roomTimeline.removeListener(cons.events.roomTimeline.EVENT_REDACTED, handleEventRedact);
|
||||
};
|
||||
}, [roomTimeline]);
|
||||
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
let jumpToItemIndex = -1;
|
||||
|
||||
function RoomViewContent({ roomInputRef, eventId, roomTimeline }) {
|
||||
const [throttle] = useState(new Throttle());
|
||||
|
||||
const timelineSVRef = useRef(null);
|
||||
const timelineScrollRef = useRef(null);
|
||||
const eventLimitRef = useRef(null);
|
||||
const [editEventId, setEditEventId] = useState(null);
|
||||
const cancelEdit = () => setEditEventId(null);
|
||||
|
||||
const readUptoEvtStore = useStore(roomTimeline);
|
||||
const [onLimitUpdate, forceUpdateLimit] = useForceUpdate();
|
||||
|
||||
const timelineInfo = useTimeline(roomTimeline, eventId, readUptoEvtStore, eventLimitRef);
|
||||
const [paginateInfo, autoPaginate] = usePaginate(
|
||||
roomTimeline,
|
||||
readUptoEvtStore,
|
||||
forceUpdateLimit,
|
||||
timelineScrollRef,
|
||||
eventLimitRef,
|
||||
);
|
||||
const [handleScroll, handleScrollToLive] = useHandleScroll(
|
||||
roomTimeline,
|
||||
autoPaginate,
|
||||
readUptoEvtStore,
|
||||
forceUpdateLimit,
|
||||
timelineScrollRef,
|
||||
eventLimitRef,
|
||||
);
|
||||
const newEvent = useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, eventLimitRef);
|
||||
|
||||
const { timeline } = roomTimeline;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!roomTimeline.initialized) {
|
||||
timelineScrollRef.current = new TimelineScroll(timelineSVRef.current);
|
||||
eventLimitRef.current = new EventLimit();
|
||||
}
|
||||
});
|
||||
|
||||
// when active timeline changes
|
||||
useEffect(() => {
|
||||
if (!roomTimeline.initialized) return undefined;
|
||||
const timelineScroll = timelineScrollRef.current;
|
||||
|
||||
if (timeline.length > 0) {
|
||||
if (jumpToItemIndex === -1) {
|
||||
timelineScroll.scrollToBottom();
|
||||
} else {
|
||||
timelineScroll.scrollToIndex(jumpToItemIndex, 80);
|
||||
}
|
||||
if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()) {
|
||||
const readUpToId = roomTimeline.getReadUpToEventId();
|
||||
if (readUptoEvtStore.getItem()?.getId() === readUpToId || readUpToId === null) {
|
||||
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
|
||||
}
|
||||
}
|
||||
jumpToItemIndex = -1;
|
||||
}
|
||||
autoPaginate();
|
||||
|
||||
roomTimeline.on(cons.events.roomTimeline.SCROLL_TO_LIVE, handleScrollToLive);
|
||||
return () => {
|
||||
if (timelineSVRef.current === null) return;
|
||||
roomTimeline.removeListener(cons.events.roomTimeline.SCROLL_TO_LIVE, handleScrollToLive);
|
||||
};
|
||||
}, [timelineInfo]);
|
||||
|
||||
// when paginating from server
|
||||
useEffect(() => {
|
||||
if (!roomTimeline.initialized) return;
|
||||
const timelineScroll = timelineScrollRef.current;
|
||||
timelineScroll.tryRestoringScroll();
|
||||
autoPaginate();
|
||||
}, [paginateInfo]);
|
||||
|
||||
// when paginating locally
|
||||
useEffect(() => {
|
||||
if (!roomTimeline.initialized) return;
|
||||
const timelineScroll = timelineScrollRef.current;
|
||||
timelineScroll.tryRestoringScroll();
|
||||
}, [onLimitUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
const timelineScroll = timelineScrollRef.current;
|
||||
if (!roomTimeline.initialized) return;
|
||||
if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward() && document.visibilityState === 'visible') {
|
||||
timelineScroll.scrollToBottom();
|
||||
} else {
|
||||
timelineScroll.tryRestoringScroll();
|
||||
}
|
||||
}, [newEvent]);
|
||||
|
||||
useResizeObserver(
|
||||
useCallback((entries) => {
|
||||
if (!roomInputRef.current) return;
|
||||
const editorBaseEntry = getResizeObserverEntry(roomInputRef.current, entries);
|
||||
if (!editorBaseEntry) return;
|
||||
|
||||
const timelineScroll = timelineScrollRef.current;
|
||||
if (!roomTimeline.initialized) return;
|
||||
if (timelineScroll.bottom < 40 && !roomTimeline.canPaginateForward() && document.visibilityState === 'visible') {
|
||||
timelineScroll.scrollToBottom();
|
||||
}
|
||||
}, [roomInputRef]),
|
||||
useCallback(() => roomInputRef.current, [roomInputRef]),
|
||||
);
|
||||
|
||||
const listenKeyboard = useCallback((event) => {
|
||||
if (event.ctrlKey || event.altKey || event.metaKey) return;
|
||||
if (event.key !== 'ArrowUp') return;
|
||||
if (navigation.isRawModalVisible) return;
|
||||
|
||||
if (document.activeElement.id !== 'message-textarea') return;
|
||||
if (document.activeElement.value !== '') return;
|
||||
|
||||
const {
|
||||
timeline: tl, activeTimeline, liveTimeline, matrixClient: mx,
|
||||
} = roomTimeline;
|
||||
const limit = eventLimitRef.current;
|
||||
if (activeTimeline !== liveTimeline) return;
|
||||
if (tl.length > limit.length) return;
|
||||
|
||||
const mTypes = ['m.text'];
|
||||
for (let i = tl.length - 1; i >= 0; i -= 1) {
|
||||
const mE = tl[i];
|
||||
if (
|
||||
mE.getSender() === mx.getUserId()
|
||||
&& mE.getType() === 'm.room.message'
|
||||
&& mTypes.includes(mE.getContent()?.msgtype)
|
||||
) {
|
||||
setEditEventId(mE.getId());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [roomTimeline]);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.addEventListener('keydown', listenKeyboard);
|
||||
return () => {
|
||||
document.body.removeEventListener('keydown', listenKeyboard);
|
||||
};
|
||||
}, [listenKeyboard]);
|
||||
|
||||
const handleTimelineScroll = (event) => {
|
||||
const timelineScroll = timelineScrollRef.current;
|
||||
if (!event.target) return;
|
||||
|
||||
throttle._(() => {
|
||||
const backwards = timelineScroll?.calcScroll();
|
||||
if (typeof backwards !== 'boolean') return;
|
||||
handleScroll(backwards);
|
||||
}, 200)();
|
||||
};
|
||||
|
||||
const renderTimeline = () => {
|
||||
const tl = [];
|
||||
const limit = eventLimitRef.current;
|
||||
|
||||
let itemCountIndex = 0;
|
||||
jumpToItemIndex = -1;
|
||||
const readUptoEvent = readUptoEvtStore.getItem();
|
||||
let unreadDivider = false;
|
||||
|
||||
if (roomTimeline.canPaginateBackward() || limit.from > 0) {
|
||||
tl.push(loadingMsgPlaceholders(1, PLACEHOLDER_COUNT));
|
||||
itemCountIndex += PLACEHOLDER_COUNT;
|
||||
}
|
||||
for (let i = limit.from; i < limit.length; i += 1) {
|
||||
if (i >= timeline.length) break;
|
||||
const mEvent = timeline[i];
|
||||
const prevMEvent = timeline[i - 1] ?? null;
|
||||
|
||||
if (i === 0 && !roomTimeline.canPaginateBackward()) {
|
||||
if (mEvent.getType() === 'm.room.create') {
|
||||
tl.push(
|
||||
<RoomIntroContainer key={mEvent.getId()} event={mEvent} timeline={roomTimeline} />,
|
||||
);
|
||||
itemCountIndex += 1;
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
} else {
|
||||
tl.push(<RoomIntroContainer key="room-intro" event={null} timeline={roomTimeline} />);
|
||||
itemCountIndex += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let isNewEvent = false;
|
||||
if (!unreadDivider) {
|
||||
unreadDivider = (readUptoEvent
|
||||
&& prevMEvent?.getTs() <= readUptoEvent.getTs()
|
||||
&& readUptoEvent.getTs() < mEvent.getTs());
|
||||
if (unreadDivider) {
|
||||
isNewEvent = true;
|
||||
tl.push(<Divider key={`new-${mEvent.getId()}`} variant="positive" text="New messages" />);
|
||||
itemCountIndex += 1;
|
||||
if (jumpToItemIndex === -1) jumpToItemIndex = itemCountIndex;
|
||||
}
|
||||
}
|
||||
const dayDivider = prevMEvent && !isInSameDay(mEvent.getDate(), prevMEvent.getDate());
|
||||
if (dayDivider) {
|
||||
tl.push(<Divider key={`divider-${mEvent.getId()}`} text={`${dateFormat(mEvent.getDate(), 'mmmm dd, yyyy')}`} />);
|
||||
itemCountIndex += 1;
|
||||
}
|
||||
|
||||
const focusId = timelineInfo.focusEventId;
|
||||
const isFocus = focusId === mEvent.getId();
|
||||
if (isFocus) jumpToItemIndex = itemCountIndex;
|
||||
|
||||
tl.push(renderEvent(
|
||||
roomTimeline,
|
||||
mEvent,
|
||||
isNewEvent ? null : prevMEvent,
|
||||
isFocus,
|
||||
editEventId === mEvent.getId(),
|
||||
setEditEventId,
|
||||
cancelEdit,
|
||||
));
|
||||
itemCountIndex += 1;
|
||||
}
|
||||
if (roomTimeline.canPaginateForward() || limit.length < timeline.length) {
|
||||
tl.push(loadingMsgPlaceholders(2, PLACEHOLDER_COUNT));
|
||||
}
|
||||
|
||||
return tl;
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView onScroll={handleTimelineScroll} ref={timelineSVRef} autoHide>
|
||||
<div className="room-view__content" onClick={handleOnClickCapture}>
|
||||
<div className="timeline__wrapper">
|
||||
{ roomTimeline.initialized ? renderTimeline() : loadingMsgPlaceholders('loading', 3) }
|
||||
</div>
|
||||
</div>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
RoomViewContent.defaultProps = {
|
||||
eventId: null,
|
||||
};
|
||||
RoomViewContent.propTypes = {
|
||||
eventId: PropTypes.string,
|
||||
roomTimeline: PropTypes.shape({}).isRequired,
|
||||
roomInputRef: PropTypes.shape({
|
||||
current: PropTypes.shape({})
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default RoomViewContent;
|
||||
@@ -1,30 +0,0 @@
|
||||
@use '../../partials/dir';
|
||||
|
||||
.room-view__content {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
|
||||
& .timeline__wrapper {
|
||||
--typing-noti-height: 28px;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
padding-bottom: var(--typing-noti-height);
|
||||
|
||||
& .message,
|
||||
& .ph-msg,
|
||||
& .timeline-change {
|
||||
@include dir.prop(border-radius,
|
||||
0 var(--bo-radius) var(--bo-radius) 0,
|
||||
var(--bo-radius) 0 0 var(--bo-radius),
|
||||
);
|
||||
}
|
||||
|
||||
& > .divider {
|
||||
margin: var(--sp-extra-tight);
|
||||
@include dir.side(margin, var(--sp-normal), var(--sp-extra-tight));
|
||||
@include dir.side(padding, calc(var(--av-small) + var(--sp-tight)), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomViewFloating.scss';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import { markAsRead } from '../../../client/action/notifications';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Button from '../../atoms/button/Button';
|
||||
|
||||
import MessageIC from '../../../../public/res/ic/outlined/message.svg';
|
||||
import MessageUnreadIC from '../../../../public/res/ic/outlined/message-unread.svg';
|
||||
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
|
||||
|
||||
import { getUsersActionJsx } from './common';
|
||||
|
||||
function useJumpToEvent(roomTimeline) {
|
||||
const [eventId, setEventId] = useState(null);
|
||||
|
||||
const jumpToEvent = () => {
|
||||
roomTimeline.loadEventTimeline(eventId);
|
||||
};
|
||||
|
||||
const cancelJumpToEvent = () => {
|
||||
markAsRead(roomTimeline.roomId);
|
||||
setEventId(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const readEventId = roomTimeline.getReadUpToEventId();
|
||||
// we only show "Jump to unread" btn only if the event is not in timeline.
|
||||
// if event is in timeline
|
||||
// we will automatically open the timeline from that event position
|
||||
if (!readEventId?.startsWith('~') && !roomTimeline.hasEventInTimeline(readEventId)) {
|
||||
setEventId(readEventId);
|
||||
}
|
||||
|
||||
const { notifications } = initMatrix;
|
||||
const handleMarkAsRead = () => setEventId(null);
|
||||
notifications.on(cons.events.notifications.FULL_READ, handleMarkAsRead);
|
||||
|
||||
return () => {
|
||||
notifications.removeListener(cons.events.notifications.FULL_READ, handleMarkAsRead);
|
||||
setEventId(null);
|
||||
};
|
||||
}, [roomTimeline]);
|
||||
|
||||
return [!!eventId, jumpToEvent, cancelJumpToEvent];
|
||||
}
|
||||
|
||||
function useTypingMembers(roomTimeline) {
|
||||
const [typingMembers, setTypingMembers] = useState(new Set());
|
||||
|
||||
const updateTyping = (members) => {
|
||||
const mx = initMatrix.matrixClient;
|
||||
members.delete(mx.getUserId());
|
||||
setTypingMembers(members);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTypingMembers(new Set());
|
||||
roomTimeline.on(cons.events.roomTimeline.TYPING_MEMBERS_UPDATED, updateTyping);
|
||||
return () => {
|
||||
roomTimeline?.removeListener(cons.events.roomTimeline.TYPING_MEMBERS_UPDATED, updateTyping);
|
||||
};
|
||||
}, [roomTimeline]);
|
||||
|
||||
return [typingMembers];
|
||||
}
|
||||
|
||||
function useScrollToBottom(roomTimeline) {
|
||||
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||
const handleAtBottom = (atBottom) => setIsAtBottom(atBottom);
|
||||
|
||||
useEffect(() => {
|
||||
setIsAtBottom(true);
|
||||
roomTimeline.on(cons.events.roomTimeline.AT_BOTTOM, handleAtBottom);
|
||||
return () => roomTimeline.removeListener(cons.events.roomTimeline.AT_BOTTOM, handleAtBottom);
|
||||
}, [roomTimeline]);
|
||||
|
||||
return [isAtBottom, setIsAtBottom];
|
||||
}
|
||||
|
||||
function RoomViewFloating({
|
||||
roomId, roomTimeline,
|
||||
}) {
|
||||
const [isJumpToEvent, jumpToEvent, cancelJumpToEvent] = useJumpToEvent(roomTimeline);
|
||||
const [typingMembers] = useTypingMembers(roomTimeline);
|
||||
const [isAtBottom, setIsAtBottom] = useScrollToBottom(roomTimeline);
|
||||
|
||||
const handleScrollToBottom = () => {
|
||||
roomTimeline.emit(cons.events.roomTimeline.SCROLL_TO_LIVE);
|
||||
setIsAtBottom(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`room-view__unread ${isJumpToEvent ? 'room-view__unread--open' : ''}`}>
|
||||
<Button iconSrc={MessageUnreadIC} onClick={jumpToEvent} variant="primary">
|
||||
<Text variant="b3" weight="medium">Jump to unread messages</Text>
|
||||
</Button>
|
||||
<Button iconSrc={TickMarkIC} onClick={cancelJumpToEvent} variant="primary">
|
||||
<Text variant="b3" weight="bold">Mark as read</Text>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={`room-view__typing${typingMembers.size > 0 ? ' room-view__typing--open' : ''}`}>
|
||||
<div className="bouncing-loader"><div /></div>
|
||||
<Text variant="b2">{getUsersActionJsx(roomId, [...typingMembers], 'typing...')}</Text>
|
||||
</div>
|
||||
<div className={`room-view__STB${isAtBottom ? '' : ' room-view__STB--open'}`}>
|
||||
<Button iconSrc={MessageIC} onClick={handleScrollToBottom}>
|
||||
<Text variant="b3" weight="medium">Jump to latest</Text>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
RoomViewFloating.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
roomTimeline: PropTypes.shape({}).isRequired,
|
||||
};
|
||||
|
||||
export default RoomViewFloating;
|
||||
@@ -1,125 +0,0 @@
|
||||
@use '../../partials/flex';
|
||||
@use '../../partials/text';
|
||||
@use '../../partials/dir';
|
||||
|
||||
.room-view {
|
||||
&__typing {
|
||||
display: flex;
|
||||
padding: var(--sp-ultra-tight) var(--sp-normal);
|
||||
background: var(--bg-surface);
|
||||
transition: transform 200ms ease-in-out;
|
||||
|
||||
& b {
|
||||
color: var(--tc-surface-high);
|
||||
}
|
||||
|
||||
& .text {
|
||||
@extend .cp-txt__ellipsis;
|
||||
@extend .cp-fx__item-one;
|
||||
|
||||
margin: 0 var(--sp-tight);
|
||||
}
|
||||
|
||||
&--open {
|
||||
transform: translateY(-99%);
|
||||
box-shadow: 0 4px 0 0 var(--bg-surface);
|
||||
& .bouncing-loader {
|
||||
& > *,
|
||||
&::after,
|
||||
&::before {
|
||||
animation: bouncing-loader 0.6s infinite alternate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bouncing-loader {
|
||||
transform: translateY(2px);
|
||||
margin: 0 calc(var(--sp-ultra-tight) / 2);
|
||||
}
|
||||
.bouncing-loader > div,
|
||||
.bouncing-loader::before,
|
||||
.bouncing-loader::after {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--tc-surface-high);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
||||
.bouncing-loader::before,
|
||||
.bouncing-loader::after {
|
||||
content: "";
|
||||
}
|
||||
|
||||
.bouncing-loader > div {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.bouncing-loader > div {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.bouncing-loader::after {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes bouncing-loader {
|
||||
to {
|
||||
opacity: 0.1;
|
||||
transform: translate3d(0, -4px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
&__STB,
|
||||
&__unread {
|
||||
overflow: hidden;
|
||||
background-color: var(--bg-surface-low);
|
||||
border-radius: var(--bo-radius);
|
||||
|
||||
& button {
|
||||
justify-content: flex-start;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
padding: 6px var(--sp-tight);
|
||||
& .ic-raw {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__STB {
|
||||
position: absolute;
|
||||
@include dir.prop(left, 50%, unset);
|
||||
@include dir.prop(right, unset, 50%);
|
||||
bottom: 0;
|
||||
box-shadow: var(--bs-surface-border);
|
||||
transition: transform 200ms ease-in-out;
|
||||
transform: translate(-50%, 100%);
|
||||
|
||||
&--open {
|
||||
transform: translate(-50%, -28px);
|
||||
}
|
||||
}
|
||||
|
||||
&__unread {
|
||||
position: absolute;
|
||||
top: var(--sp-extra-tight);
|
||||
@include dir.prop(left, var(--sp-normal), unset);
|
||||
@include dir.prop(right, unset, var(--sp-normal));
|
||||
z-index: 999;
|
||||
|
||||
display: none;
|
||||
width: calc(100% - var(--sp-extra-loose));
|
||||
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 20%);
|
||||
|
||||
&--open {
|
||||
display: flex;
|
||||
}
|
||||
& button:first-child {
|
||||
@extend .cp-fx__item-one;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomViewHeader.scss';
|
||||
|
||||
import { twemojify } from '../../../util/twemojify';
|
||||
import { blurOnBubbling } from '../../atoms/button/script';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import {
|
||||
toggleRoomSettings,
|
||||
openReusableContextMenu,
|
||||
openNavigation,
|
||||
} from '../../../client/action/navigation';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
|
||||
import { tabText } from './RoomSettings';
|
||||
import Text from '../../atoms/text/Text';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import Header, { TitleWrapper } from '../../atoms/header/Header';
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
import RoomOptions from '../../molecules/room-options/RoomOptions';
|
||||
|
||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
||||
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
|
||||
import BackArrowIC from '../../../../public/res/ic/outlined/chevron-left.svg';
|
||||
|
||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||
import { useSetSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
|
||||
function RoomViewHeader({ roomId }) {
|
||||
const [, forceUpdate] = useForceUpdate();
|
||||
const mx = initMatrix.matrixClient;
|
||||
const isDM = initMatrix.roomList.directs.has(roomId);
|
||||
const room = mx.getRoom(roomId);
|
||||
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
|
||||
let avatarSrc = room.getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
|
||||
avatarSrc = isDM
|
||||
? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop')
|
||||
: avatarSrc;
|
||||
const roomName = room.name;
|
||||
|
||||
const roomHeaderBtnRef = useRef(null);
|
||||
useEffect(() => {
|
||||
const settingsToggle = (isVisibile) => {
|
||||
const rawIcon = roomHeaderBtnRef.current.lastElementChild;
|
||||
rawIcon.style.transform = isVisibile ? 'rotateX(180deg)' : 'rotateX(0deg)';
|
||||
};
|
||||
navigation.on(cons.events.navigation.ROOM_SETTINGS_TOGGLED, settingsToggle);
|
||||
return () => {
|
||||
navigation.removeListener(cons.events.navigation.ROOM_SETTINGS_TOGGLED, settingsToggle);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const { roomList } = initMatrix;
|
||||
const handleProfileUpdate = (rId) => {
|
||||
if (roomId !== rId) return;
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
roomList.on(cons.events.roomList.ROOM_PROFILE_UPDATED, handleProfileUpdate);
|
||||
return () => {
|
||||
roomList.removeListener(cons.events.roomList.ROOM_PROFILE_UPDATED, handleProfileUpdate);
|
||||
};
|
||||
}, [roomId]);
|
||||
|
||||
const openRoomOptions = (e) => {
|
||||
openReusableContextMenu('bottom', getEventCords(e, '.ic-btn'), (closeMenu) => (
|
||||
<RoomOptions roomId={roomId} afterOptionSelect={closeMenu} />
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<Header>
|
||||
<IconButton
|
||||
src={BackArrowIC}
|
||||
className="room-header__back-btn"
|
||||
tooltip="Return to navigation"
|
||||
onClick={() => openNavigation()}
|
||||
/>
|
||||
<button
|
||||
ref={roomHeaderBtnRef}
|
||||
className="room-header__btn"
|
||||
onClick={() => toggleRoomSettings()}
|
||||
type="button"
|
||||
onMouseUp={(e) => blurOnBubbling(e, '.room-header__btn')}
|
||||
>
|
||||
<Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="small" />
|
||||
<TitleWrapper>
|
||||
<Text variant="h2" weight="medium" primary>
|
||||
{twemojify(roomName)}
|
||||
</Text>
|
||||
</TitleWrapper>
|
||||
<RawIcon src={ChevronBottomIC} />
|
||||
</button>
|
||||
{mx.isRoomEncrypted(roomId) === false && (
|
||||
<IconButton
|
||||
onClick={() => toggleRoomSettings(tabText.SEARCH)}
|
||||
tooltip="Search"
|
||||
src={SearchIC}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
className="room-header__drawer-btn"
|
||||
onClick={() => {
|
||||
setPeopleDrawer((t) => !t);
|
||||
}}
|
||||
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>
|
||||
);
|
||||
}
|
||||
RoomViewHeader.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RoomViewHeader;
|
||||
@@ -1,47 +0,0 @@
|
||||
@use '../../partials/flex';
|
||||
@use '../../partials/dir';
|
||||
@use '../../partials/screen';
|
||||
|
||||
.room-header__btn {
|
||||
min-width: 0;
|
||||
@extend .cp-fx__row--s-c;
|
||||
@include dir.side(margin, 0, auto);
|
||||
border-radius: var(--bo-radius);
|
||||
cursor: pointer;
|
||||
|
||||
& .ic-raw {
|
||||
@include dir.side(margin, 0, var(--sp-extra-tight));
|
||||
transition: transform 200ms ease-in-out;
|
||||
}
|
||||
@media (hover:hover) {
|
||||
&:hover {
|
||||
background-color: var(--bg-surface-hover);
|
||||
box-shadow: var(--bs-surface-outline);
|
||||
}
|
||||
}
|
||||
&:focus,
|
||||
&:active {
|
||||
background-color: var(--bg-surface-active);
|
||||
box-shadow: var(--bs-surface-outline);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.room-header__drawer-btn {
|
||||
@include screen.smallerThan(tabletBreakpoint) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.room-header__members-btn {
|
||||
@include screen.biggerThan(tabletBreakpoint) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.room-header__back-btn {
|
||||
@include dir.side(margin, 0, var(--sp-tight));
|
||||
|
||||
@include screen.biggerThan(mobileBreakpoint) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,491 +0,0 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomViewInput.scss';
|
||||
|
||||
import TextareaAutosize from 'react-autosize-textarea';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import settings from '../../../client/state/settings';
|
||||
import { openEmojiBoard, openReusableContextMenu } from '../../../client/action/navigation';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import { bytesToSize, getEventCords } from '../../../util/common';
|
||||
import { getUsername } from '../../../util/matrixUtil';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||
import { MessageReply } from '../../molecules/message/Message';
|
||||
|
||||
import StickerBoard from '../sticker-board/StickerBoard';
|
||||
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
||||
|
||||
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
|
||||
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
|
||||
import SendIC from '../../../../public/res/ic/outlined/send.svg';
|
||||
import StickerIC from '../../../../public/res/ic/outlined/sticker.svg';
|
||||
import ShieldIC from '../../../../public/res/ic/outlined/shield.svg';
|
||||
import VLCIC from '../../../../public/res/ic/outlined/vlc.svg';
|
||||
import VolumeFullIC from '../../../../public/res/ic/outlined/volume-full.svg';
|
||||
import FileIC from '../../../../public/res/ic/outlined/file.svg';
|
||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||
|
||||
import commands from './commands';
|
||||
|
||||
const CMD_REGEX = /(^\/|:|@)(\S*)$/;
|
||||
let isTyping = false;
|
||||
let isCmdActivated = false;
|
||||
let cmdCursorPos = null;
|
||||
function RoomViewInput({
|
||||
roomId, roomTimeline, viewEvent,
|
||||
}) {
|
||||
const [attachment, setAttachment] = useState(null);
|
||||
const [replyTo, setReplyTo] = useState(null);
|
||||
|
||||
const textAreaRef = useRef(null);
|
||||
const inputBaseRef = useRef(null);
|
||||
const uploadInputRef = useRef(null);
|
||||
const uploadProgressRef = useRef(null);
|
||||
const rightOptionsRef = useRef(null);
|
||||
|
||||
const TYPING_TIMEOUT = 5000;
|
||||
const mx = initMatrix.matrixClient;
|
||||
const { roomsInput } = initMatrix;
|
||||
|
||||
function requestFocusInput() {
|
||||
if (textAreaRef === null) return;
|
||||
textAreaRef.current.focus();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
roomsInput.on(cons.events.roomsInput.ATTACHMENT_SET, setAttachment);
|
||||
viewEvent.on('focus_msg_input', requestFocusInput);
|
||||
return () => {
|
||||
roomsInput.removeListener(cons.events.roomsInput.ATTACHMENT_SET, setAttachment);
|
||||
viewEvent.removeListener('focus_msg_input', requestFocusInput);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const sendIsTyping = (isT) => {
|
||||
mx.sendTyping(roomId, isT, isT ? TYPING_TIMEOUT : undefined);
|
||||
isTyping = isT;
|
||||
|
||||
if (isT === true) {
|
||||
setTimeout(() => {
|
||||
if (isTyping) sendIsTyping(false);
|
||||
}, TYPING_TIMEOUT);
|
||||
}
|
||||
};
|
||||
|
||||
function uploadingProgress(myRoomId, { loaded, total }) {
|
||||
if (myRoomId !== roomId) return;
|
||||
const progressPer = Math.round((loaded * 100) / total);
|
||||
uploadProgressRef.current.textContent = `Uploading: ${bytesToSize(loaded)}/${bytesToSize(total)} (${progressPer}%)`;
|
||||
inputBaseRef.current.style.backgroundImage = `linear-gradient(90deg, var(--bg-surface-hover) ${progressPer}%, var(--bg-surface-low) ${progressPer}%)`;
|
||||
}
|
||||
function clearAttachment(myRoomId) {
|
||||
if (roomId !== myRoomId) return;
|
||||
setAttachment(null);
|
||||
inputBaseRef.current.style.backgroundImage = 'unset';
|
||||
uploadInputRef.current.value = null;
|
||||
}
|
||||
|
||||
function rightOptionsA11Y(A11Y) {
|
||||
const rightOptions = rightOptionsRef.current.children;
|
||||
for (let index = 0; index < rightOptions.length; index += 1) {
|
||||
rightOptions[index].tabIndex = A11Y ? 0 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
function activateCmd(prefix) {
|
||||
isCmdActivated = true;
|
||||
rightOptionsA11Y(false);
|
||||
viewEvent.emit('cmd_activate', prefix);
|
||||
}
|
||||
function deactivateCmd() {
|
||||
isCmdActivated = false;
|
||||
cmdCursorPos = null;
|
||||
rightOptionsA11Y(true);
|
||||
}
|
||||
function deactivateCmdAndEmit() {
|
||||
deactivateCmd();
|
||||
viewEvent.emit('cmd_deactivate');
|
||||
}
|
||||
function setCursorPosition(pos) {
|
||||
setTimeout(() => {
|
||||
textAreaRef.current.focus();
|
||||
textAreaRef.current.setSelectionRange(pos, pos);
|
||||
}, 0);
|
||||
}
|
||||
function replaceCmdWith(msg, cursor, replacement) {
|
||||
if (msg === null) return null;
|
||||
const targetInput = msg.slice(0, cursor);
|
||||
const cmdParts = targetInput.match(CMD_REGEX);
|
||||
const leadingInput = msg.slice(0, cmdParts.index);
|
||||
if (replacement.length > 0) setCursorPosition(leadingInput.length + replacement.length);
|
||||
return leadingInput + replacement + msg.slice(cursor);
|
||||
}
|
||||
function firedCmd(cmdData) {
|
||||
const msg = textAreaRef.current.value;
|
||||
textAreaRef.current.value = replaceCmdWith(
|
||||
msg,
|
||||
cmdCursorPos,
|
||||
typeof cmdData?.replace !== 'undefined' ? cmdData.replace : '',
|
||||
);
|
||||
deactivateCmd();
|
||||
}
|
||||
|
||||
function focusInput() {
|
||||
if (settings.isTouchScreenDevice) return;
|
||||
textAreaRef.current.focus();
|
||||
}
|
||||
|
||||
function setUpReply(userId, eventId, body, formattedBody) {
|
||||
setReplyTo({ userId, eventId, body });
|
||||
roomsInput.setReplyTo(roomId, {
|
||||
userId, eventId, body, formattedBody,
|
||||
});
|
||||
focusInput();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
roomsInput.on(cons.events.roomsInput.UPLOAD_PROGRESS_CHANGES, uploadingProgress);
|
||||
roomsInput.on(cons.events.roomsInput.ATTACHMENT_CANCELED, clearAttachment);
|
||||
roomsInput.on(cons.events.roomsInput.FILE_UPLOADED, clearAttachment);
|
||||
viewEvent.on('cmd_fired', firedCmd);
|
||||
navigation.on(cons.events.navigation.REPLY_TO_CLICKED, setUpReply);
|
||||
if (textAreaRef?.current !== null) {
|
||||
isTyping = false;
|
||||
textAreaRef.current.value = roomsInput.getMessage(roomId);
|
||||
setAttachment(roomsInput.getAttachment(roomId));
|
||||
setReplyTo(roomsInput.getReplyTo(roomId));
|
||||
}
|
||||
return () => {
|
||||
roomsInput.removeListener(cons.events.roomsInput.UPLOAD_PROGRESS_CHANGES, uploadingProgress);
|
||||
roomsInput.removeListener(cons.events.roomsInput.ATTACHMENT_CANCELED, clearAttachment);
|
||||
roomsInput.removeListener(cons.events.roomsInput.FILE_UPLOADED, clearAttachment);
|
||||
viewEvent.removeListener('cmd_fired', firedCmd);
|
||||
navigation.removeListener(cons.events.navigation.REPLY_TO_CLICKED, setUpReply);
|
||||
if (isCmdActivated) deactivateCmd();
|
||||
if (textAreaRef?.current === null) return;
|
||||
|
||||
const msg = textAreaRef.current.value;
|
||||
textAreaRef.current.style.height = 'unset';
|
||||
inputBaseRef.current.style.backgroundImage = 'unset';
|
||||
if (msg.trim() === '') {
|
||||
roomsInput.setMessage(roomId, '');
|
||||
return;
|
||||
}
|
||||
roomsInput.setMessage(roomId, msg);
|
||||
};
|
||||
}, [roomId]);
|
||||
|
||||
const sendBody = async (body, options) => {
|
||||
const opt = options ?? {};
|
||||
if (!opt.msgType) opt.msgType = 'm.text';
|
||||
if (typeof opt.autoMarkdown !== 'boolean') opt.autoMarkdown = true;
|
||||
if (roomsInput.isSending(roomId)) return;
|
||||
sendIsTyping(false);
|
||||
|
||||
roomsInput.setMessage(roomId, body);
|
||||
if (attachment !== null) {
|
||||
roomsInput.setAttachment(roomId, attachment);
|
||||
}
|
||||
textAreaRef.current.disabled = true;
|
||||
textAreaRef.current.style.cursor = 'not-allowed';
|
||||
await roomsInput.sendInput(roomId, opt);
|
||||
textAreaRef.current.disabled = false;
|
||||
textAreaRef.current.style.cursor = 'unset';
|
||||
focusInput();
|
||||
|
||||
textAreaRef.current.value = roomsInput.getMessage(roomId);
|
||||
textAreaRef.current.style.height = 'unset';
|
||||
if (replyTo !== null) setReplyTo(null);
|
||||
};
|
||||
|
||||
/** Return true if a command was executed. */
|
||||
const processCommand = async (cmdBody) => {
|
||||
const spaceIndex = cmdBody.indexOf(' ');
|
||||
const cmdName = cmdBody.slice(1, spaceIndex > -1 ? spaceIndex : undefined);
|
||||
const cmdData = spaceIndex > -1 ? cmdBody.slice(spaceIndex + 1) : '';
|
||||
if (!commands[cmdName]) {
|
||||
const sendAsMessage = await confirmDialog('Invalid Command', `"${cmdName}" is not a valid command. Did you mean to send this as a message?`, 'Send as message');
|
||||
if (sendAsMessage) {
|
||||
sendBody(cmdBody);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (['me', 'shrug', 'plain'].includes(cmdName)) {
|
||||
commands[cmdName].exe(roomId, cmdData, sendBody);
|
||||
return true;
|
||||
}
|
||||
commands[cmdName].exe(roomId, cmdData);
|
||||
return true;
|
||||
};
|
||||
|
||||
const sendMessage = async () => {
|
||||
requestAnimationFrame(() => deactivateCmdAndEmit());
|
||||
const msgBody = textAreaRef.current.value.trim();
|
||||
if (msgBody.startsWith('/')) {
|
||||
const executed = await processCommand(msgBody.trim());
|
||||
if (executed) {
|
||||
textAreaRef.current.value = '';
|
||||
textAreaRef.current.style.height = 'unset';
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (msgBody === '' && attachment === null) return;
|
||||
sendBody(msgBody);
|
||||
};
|
||||
|
||||
const handleSendSticker = async (data) => {
|
||||
roomsInput.sendSticker(roomId, data);
|
||||
};
|
||||
|
||||
function processTyping(msg) {
|
||||
const isEmptyMsg = msg === '';
|
||||
|
||||
if (isEmptyMsg && isTyping) {
|
||||
sendIsTyping(false);
|
||||
return;
|
||||
}
|
||||
if (!isEmptyMsg && !isTyping) {
|
||||
sendIsTyping(true);
|
||||
}
|
||||
}
|
||||
|
||||
function getCursorPosition() {
|
||||
return textAreaRef.current.selectionStart;
|
||||
}
|
||||
|
||||
function recognizeCmd(rawInput) {
|
||||
const cursor = getCursorPosition();
|
||||
const targetInput = rawInput.slice(0, cursor);
|
||||
|
||||
const cmdParts = targetInput.match(CMD_REGEX);
|
||||
if (cmdParts === null) {
|
||||
if (isCmdActivated) deactivateCmdAndEmit();
|
||||
return;
|
||||
}
|
||||
const cmdPrefix = cmdParts[1];
|
||||
const cmdSlug = cmdParts[2];
|
||||
|
||||
if (cmdPrefix === ':') {
|
||||
// skip emoji autofill command if link is suspected.
|
||||
const checkForLink = targetInput.slice(0, cmdParts.index);
|
||||
if (checkForLink.match(/(http|https|mailto|matrix|ircs|irc)$/)) {
|
||||
deactivateCmdAndEmit();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cmdCursorPos = cursor;
|
||||
if (cmdSlug === '') {
|
||||
activateCmd(cmdPrefix);
|
||||
return;
|
||||
}
|
||||
if (!isCmdActivated) activateCmd(cmdPrefix);
|
||||
viewEvent.emit('cmd_process', cmdPrefix, cmdSlug);
|
||||
}
|
||||
|
||||
const handleMsgTyping = (e) => {
|
||||
const msg = e.target.value;
|
||||
recognizeCmd(e.target.value);
|
||||
if (!isCmdActivated) processTyping(msg);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
roomsInput.cancelReplyTo(roomId);
|
||||
setReplyTo(null);
|
||||
}
|
||||
if (e.key === 'Enter' && e.shiftKey === false) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaste = (e) => {
|
||||
if (e.clipboardData === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.clipboardData.items === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < e.clipboardData.items.length; i += 1) {
|
||||
const item = e.clipboardData.items[i];
|
||||
if (item.type.indexOf('image') !== -1) {
|
||||
const image = item.getAsFile();
|
||||
if (attachment === null) {
|
||||
setAttachment(image);
|
||||
if (image !== null) {
|
||||
roomsInput.setAttachment(roomId, image);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function addEmoji(emoji) {
|
||||
textAreaRef.current.value += emoji.unicode;
|
||||
textAreaRef.current.focus();
|
||||
}
|
||||
|
||||
const handleUploadClick = () => {
|
||||
if (attachment === null) uploadInputRef.current.click();
|
||||
else {
|
||||
roomsInput.cancelAttachment(roomId);
|
||||
}
|
||||
};
|
||||
function uploadFileChange(e) {
|
||||
const file = e.target.files.item(0);
|
||||
setAttachment(file);
|
||||
if (file !== null) roomsInput.setAttachment(roomId, file);
|
||||
}
|
||||
|
||||
function renderInputs() {
|
||||
const canISend = roomTimeline.room.currentState.maySendMessage(mx.getUserId());
|
||||
const tombstoneEvent = roomTimeline.room.currentState.getStateEvents('m.room.tombstone')[0];
|
||||
if (!canISend || tombstoneEvent) {
|
||||
return (
|
||||
<Text className="room-input__alert">
|
||||
{
|
||||
tombstoneEvent
|
||||
? tombstoneEvent.getContent()?.body ?? 'This room has been replaced and is no longer active.'
|
||||
: 'You do not have permission to post to this room'
|
||||
}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className={`room-input__option-container${attachment === null ? '' : ' room-attachment__option'}`}>
|
||||
<input onChange={uploadFileChange} style={{ display: 'none' }} ref={uploadInputRef} type="file" />
|
||||
<IconButton onClick={handleUploadClick} tooltip={attachment === null ? 'Upload' : 'Cancel'} src={CirclePlusIC} />
|
||||
</div>
|
||||
<div ref={inputBaseRef} className="room-input__input-container">
|
||||
{roomTimeline.isEncrypted() && <RawIcon size="extra-small" src={ShieldIC} />}
|
||||
<ScrollView autoHide>
|
||||
<Text className="room-input__textarea-wrapper">
|
||||
<TextareaAutosize
|
||||
dir="auto"
|
||||
id="message-textarea"
|
||||
ref={textAreaRef}
|
||||
onChange={handleMsgTyping}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Send a message..."
|
||||
/>
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</div>
|
||||
<div ref={rightOptionsRef} className="room-input__option-container">
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
openReusableContextMenu(
|
||||
'top',
|
||||
(() => {
|
||||
const cords = getEventCords(e);
|
||||
cords.y -= 20;
|
||||
return cords;
|
||||
})(),
|
||||
(closeMenu) => (
|
||||
<StickerBoard
|
||||
roomId={roomId}
|
||||
onSelect={(data) => {
|
||||
handleSendSticker(data);
|
||||
closeMenu();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
);
|
||||
}}
|
||||
tooltip="Sticker"
|
||||
src={StickerIC}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
const cords = getEventCords(e);
|
||||
cords.x += (document.dir === 'rtl' ? -80 : 80);
|
||||
cords.y -= 250;
|
||||
openEmojiBoard(cords, addEmoji);
|
||||
}}
|
||||
tooltip="Emoji"
|
||||
src={EmojiIC}
|
||||
/>
|
||||
<IconButton onClick={sendMessage} tooltip="Send" src={SendIC} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function attachFile() {
|
||||
const fileType = attachment.type.slice(0, attachment.type.indexOf('/'));
|
||||
return (
|
||||
<div className="room-attachment">
|
||||
<div className={`room-attachment__preview${fileType !== 'image' ? ' room-attachment__icon' : ''}`}>
|
||||
{fileType === 'image' && <img alt={attachment.name} src={URL.createObjectURL(attachment)} />}
|
||||
{fileType === 'video' && <RawIcon src={VLCIC} />}
|
||||
{fileType === 'audio' && <RawIcon src={VolumeFullIC} />}
|
||||
{fileType !== 'image' && fileType !== 'video' && fileType !== 'audio' && <RawIcon src={FileIC} />}
|
||||
</div>
|
||||
<div className="room-attachment__info">
|
||||
<Text variant="b1">{attachment.name}</Text>
|
||||
<Text variant="b3"><span ref={uploadProgressRef}>{`size: ${bytesToSize(attachment.size)}`}</span></Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function attachReply() {
|
||||
return (
|
||||
<div className="room-reply">
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
roomsInput.cancelReplyTo(roomId);
|
||||
setReplyTo(null);
|
||||
}}
|
||||
src={CrossIC}
|
||||
tooltip="Cancel reply"
|
||||
size="extra-small"
|
||||
/>
|
||||
<MessageReply
|
||||
userId={replyTo.userId}
|
||||
onKeyDown={handleKeyDown}
|
||||
name={getUsername(replyTo.userId)}
|
||||
color={colorMXID(replyTo.userId)}
|
||||
body={replyTo.body}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{ replyTo !== null && attachReply()}
|
||||
{ attachment !== null && attachFile() }
|
||||
<form className="room-input" onSubmit={(e) => { e.preventDefault(); }}>
|
||||
{
|
||||
renderInputs()
|
||||
}
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
RoomViewInput.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
roomTimeline: PropTypes.shape({}).isRequired,
|
||||
viewEvent: PropTypes.shape({}).isRequired,
|
||||
};
|
||||
|
||||
export default RoomViewInput;
|
||||
@@ -1,108 +0,0 @@
|
||||
@use '../../partials/dir';
|
||||
|
||||
.room-input {
|
||||
padding: var(--sp-extra-tight) calc(var(--sp-normal) - 2px);
|
||||
display: flex;
|
||||
min-height: 56px;
|
||||
|
||||
&__alert {
|
||||
margin: auto;
|
||||
padding: 0 var(--sp-tight);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__input-container {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
margin: 0 calc(var(--sp-tight) - 2px);
|
||||
background-color: var(--bg-surface-low);
|
||||
box-shadow: var(--bs-surface-border);
|
||||
border-radius: var(--bo-radius);
|
||||
|
||||
& > .ic-raw {
|
||||
transform: scale(0.8);
|
||||
margin: 0 var(--sp-extra-tight);
|
||||
}
|
||||
|
||||
& .scrollbar {
|
||||
max-height: 50vh;
|
||||
flex: 1;
|
||||
|
||||
&:first-child {
|
||||
@include dir.side(margin, var(--sp-tight), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__textarea-wrapper {
|
||||
min-height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& textarea {
|
||||
resize: none;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 100%;
|
||||
padding: var(--sp-ultra-tight) 0;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--tc-surface-low);
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.room-attachment {
|
||||
--side-spacing: calc(var(--sp-normal) + var(--av-small) + var(--sp-tight));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include dir.side(margin, var(--side-spacing), 0);
|
||||
margin-top: var(--sp-extra-tight);
|
||||
line-height: 0;
|
||||
|
||||
&__preview > img {
|
||||
max-height: 40px;
|
||||
border-radius: var(--bo-radius);
|
||||
max-width: 150px;
|
||||
}
|
||||
&__icon {
|
||||
padding: var(--sp-extra-tight);
|
||||
background-color: var(--bg-surface-low);
|
||||
box-shadow: var(--bs-surface-border);
|
||||
border-radius: var(--bo-radius);
|
||||
}
|
||||
&__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0 var(--sp-tight);
|
||||
}
|
||||
|
||||
&__option button {
|
||||
transition: transform 200ms ease-in-out;
|
||||
transform: translateY(-48px);
|
||||
& .ic-raw {
|
||||
transition: transform 200ms ease-in-out;
|
||||
transform: rotate(45deg);
|
||||
background-color: var(--bg-caution);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.room-reply {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--bg-surface-low);
|
||||
border-bottom: 1px solid var(--bg-surface-border);
|
||||
|
||||
& .ic-btn-surface {
|
||||
@include dir.side(margin, 17px, 13px);
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import { getScrollInfo } from '../../../util/common';
|
||||
|
||||
class TimelineScroll {
|
||||
constructor(target) {
|
||||
if (target === null) {
|
||||
throw new Error('Can not initialize TimelineScroll, target HTMLElement in null');
|
||||
}
|
||||
this.scroll = target;
|
||||
|
||||
this.backwards = false;
|
||||
this.inTopHalf = false;
|
||||
|
||||
this.isScrollable = false;
|
||||
this.top = 0;
|
||||
this.bottom = 0;
|
||||
this.height = 0;
|
||||
this.viewHeight = 0;
|
||||
|
||||
this.topMsg = null;
|
||||
this.bottomMsg = null;
|
||||
this.diff = 0;
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
const scrollInfo = getScrollInfo(this.scroll);
|
||||
const maxScrollTop = scrollInfo.height - scrollInfo.viewHeight;
|
||||
|
||||
this._scrollTo(scrollInfo, maxScrollTop);
|
||||
}
|
||||
|
||||
// use previous calc by this._updateTopBottomMsg() & this._calcDiff.
|
||||
tryRestoringScroll() {
|
||||
const scrollInfo = getScrollInfo(this.scroll);
|
||||
|
||||
let scrollTop = 0;
|
||||
const ot = this.inTopHalf ? this.topMsg?.offsetTop : this.bottomMsg?.offsetTop;
|
||||
if (!ot) scrollTop = Math.round(this.height - this.viewHeight);
|
||||
else scrollTop = ot - this.diff;
|
||||
|
||||
this._scrollTo(scrollInfo, scrollTop);
|
||||
}
|
||||
|
||||
scrollToIndex(index, offset = 0) {
|
||||
const scrollInfo = getScrollInfo(this.scroll);
|
||||
const msgs = this.scroll.lastElementChild.lastElementChild.children;
|
||||
const offsetTop = msgs[index]?.offsetTop;
|
||||
|
||||
if (offsetTop === undefined) return;
|
||||
// if msg is already in visible are we don't need to scroll to that
|
||||
if (offsetTop > scrollInfo.top && offsetTop < (scrollInfo.top + scrollInfo.viewHeight)) return;
|
||||
const to = offsetTop - offset;
|
||||
|
||||
this._scrollTo(scrollInfo, to);
|
||||
}
|
||||
|
||||
_scrollTo(scrollInfo, scrollTop) {
|
||||
this.scroll.scrollTop = scrollTop;
|
||||
|
||||
// browser emit 'onscroll' event only if the 'element.scrollTop' value changes.
|
||||
// so here we flag that the upcoming 'onscroll' event is
|
||||
// emitted as side effect of assigning 'this.scroll.scrollTop' above
|
||||
// only if it's changes.
|
||||
// by doing so we prevent this._updateCalc() from calc again.
|
||||
if (scrollTop !== this.top) {
|
||||
this.scrolledByCode = true;
|
||||
}
|
||||
const sInfo = { ...scrollInfo };
|
||||
|
||||
const maxScrollTop = scrollInfo.height - scrollInfo.viewHeight;
|
||||
|
||||
sInfo.top = (scrollTop > maxScrollTop) ? maxScrollTop : scrollTop;
|
||||
this._updateCalc(sInfo);
|
||||
}
|
||||
|
||||
// we maintain reference of top and bottom messages
|
||||
// to restore the scroll position when
|
||||
// messages gets removed from either end and added to other.
|
||||
_updateTopBottomMsg() {
|
||||
const msgs = this.scroll.lastElementChild.lastElementChild.children;
|
||||
const lMsgIndex = msgs.length - 1;
|
||||
|
||||
// TODO: classname 'ph-msg' prevent this class from being used
|
||||
const PLACEHOLDER_COUNT = 2;
|
||||
this.topMsg = msgs[0]?.className === 'ph-msg'
|
||||
? msgs[PLACEHOLDER_COUNT]
|
||||
: msgs[0];
|
||||
this.bottomMsg = msgs[lMsgIndex]?.className === 'ph-msg'
|
||||
? msgs[lMsgIndex - PLACEHOLDER_COUNT]
|
||||
: msgs[lMsgIndex];
|
||||
}
|
||||
|
||||
// we calculate the difference between first/last message and current scrollTop.
|
||||
// if we are going above we calc diff between first and scrollTop
|
||||
// else otherwise.
|
||||
// NOTE: This will help to restore the scroll when msgs get's removed
|
||||
// from one end and added to other end
|
||||
_calcDiff(scrollInfo) {
|
||||
if (!this.topMsg || !this.bottomMsg) return 0;
|
||||
if (this.inTopHalf) {
|
||||
return this.topMsg.offsetTop - scrollInfo.top;
|
||||
}
|
||||
return this.bottomMsg.offsetTop - scrollInfo.top;
|
||||
}
|
||||
|
||||
_updateCalc(scrollInfo) {
|
||||
const halfViewHeight = Math.round(scrollInfo.viewHeight / 2);
|
||||
const scrollMiddle = scrollInfo.top + halfViewHeight;
|
||||
const lastMiddle = this.top + halfViewHeight;
|
||||
|
||||
this.backwards = scrollMiddle < lastMiddle;
|
||||
this.inTopHalf = scrollMiddle < scrollInfo.height / 2;
|
||||
|
||||
this.isScrollable = scrollInfo.isScrollable;
|
||||
this.top = scrollInfo.top;
|
||||
this.bottom = scrollInfo.height - (scrollInfo.top + scrollInfo.viewHeight);
|
||||
this.height = scrollInfo.height;
|
||||
this.viewHeight = scrollInfo.viewHeight;
|
||||
|
||||
this._updateTopBottomMsg();
|
||||
this.diff = this._calcDiff(scrollInfo);
|
||||
}
|
||||
|
||||
calcScroll() {
|
||||
if (this.scrolledByCode) {
|
||||
this.scrolledByCode = false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const scrollInfo = getScrollInfo(this.scroll);
|
||||
this._updateCalc(scrollInfo);
|
||||
|
||||
return this.backwards;
|
||||
}
|
||||
}
|
||||
|
||||
export default TimelineScroll;
|
||||
@@ -1,220 +0,0 @@
|
||||
import React from 'react';
|
||||
import './commands.scss';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import * as roomActions from '../../../client/action/room';
|
||||
import { hasDMWith, hasDevices } from '../../../util/matrixUtil';
|
||||
import { selectRoom, openReusableDialog } from '../../../client/action/navigation';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import SettingTile from '../../molecules/setting-tile/SettingTile';
|
||||
|
||||
const MXID_REG = /^@\S+:\S+$/;
|
||||
const ROOM_ID_ALIAS_REG = /^(#|!)\S+:\S+$/;
|
||||
const ROOM_ID_REG = /^!\S+:\S+$/;
|
||||
const MXC_REG = /^mxc:\/\/\S+$/;
|
||||
|
||||
export function processMxidAndReason(data) {
|
||||
let reason;
|
||||
let idData = data;
|
||||
const reasonMatch = data.match(/\s-r\s/);
|
||||
if (reasonMatch) {
|
||||
idData = data.slice(0, reasonMatch.index);
|
||||
reason = data.slice(reasonMatch.index + reasonMatch[0].length);
|
||||
if (reason.trim() === '') reason = undefined;
|
||||
}
|
||||
const rawIds = idData.split(' ');
|
||||
const userIds = rawIds.filter((id) => id.match(MXID_REG));
|
||||
return {
|
||||
userIds,
|
||||
reason,
|
||||
};
|
||||
}
|
||||
|
||||
const commands = {
|
||||
me: {
|
||||
name: 'me',
|
||||
description: 'Display action',
|
||||
exe: (roomId, data, onSuccess) => {
|
||||
const body = data.trim();
|
||||
if (body === '') return;
|
||||
onSuccess(body, { msgType: 'm.emote' });
|
||||
},
|
||||
},
|
||||
shrug: {
|
||||
name: 'shrug',
|
||||
description: 'Send ¯\\_(ツ)_/¯ as message',
|
||||
exe: (roomId, data, onSuccess) => onSuccess(
|
||||
`¯\\_(ツ)_/¯${data.trim() !== '' ? ` ${data}` : ''}`,
|
||||
{ msgType: 'm.text' },
|
||||
),
|
||||
},
|
||||
plain: {
|
||||
name: 'plain',
|
||||
description: 'Send plain text message',
|
||||
exe: (roomId, data, onSuccess) => {
|
||||
const body = data.trim();
|
||||
if (body === '') return;
|
||||
onSuccess(body, { msgType: 'm.text', autoMarkdown: false });
|
||||
},
|
||||
},
|
||||
help: {
|
||||
name: 'help',
|
||||
description: 'View all commands',
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
exe: () => openHelpDialog(),
|
||||
},
|
||||
startdm: {
|
||||
name: 'startdm',
|
||||
description: 'Start direct message with user. Example: /startdm userId1',
|
||||
exe: async (roomId, data) => {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const rawIds = data.split(' ');
|
||||
const userIds = rawIds.filter((id) => id.match(MXID_REG) && id !== mx.getUserId());
|
||||
if (userIds.length === 0) return;
|
||||
if (userIds.length === 1) {
|
||||
const dmRoomId = hasDMWith(userIds[0]);
|
||||
if (dmRoomId) {
|
||||
selectRoom(dmRoomId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const devices = await Promise.all(userIds.map(hasDevices));
|
||||
const isEncrypt = devices.every((hasDevice) => hasDevice);
|
||||
const result = await roomActions.createDM(userIds, isEncrypt);
|
||||
selectRoom(result.room_id);
|
||||
},
|
||||
},
|
||||
join: {
|
||||
name: 'join',
|
||||
description: 'Join room with address. Example: /join address1 address2',
|
||||
exe: (roomId, data) => {
|
||||
const rawIds = data.split(' ');
|
||||
const roomIds = rawIds.filter((id) => id.match(ROOM_ID_ALIAS_REG));
|
||||
roomIds.map((id) => roomActions.join(id));
|
||||
},
|
||||
},
|
||||
leave: {
|
||||
name: 'leave',
|
||||
description: 'Leave current room.',
|
||||
exe: (roomId, data) => {
|
||||
if (data.trim() === '') {
|
||||
roomActions.leave(roomId);
|
||||
return;
|
||||
}
|
||||
const rawIds = data.split(' ');
|
||||
const roomIds = rawIds.filter((id) => id.match(ROOM_ID_REG));
|
||||
roomIds.map((id) => roomActions.leave(id));
|
||||
},
|
||||
},
|
||||
invite: {
|
||||
name: 'invite',
|
||||
description: 'Invite user to room. Example: /invite userId1 userId2 [-r reason]',
|
||||
exe: (roomId, data) => {
|
||||
const { userIds, reason } = processMxidAndReason(data);
|
||||
userIds.map((id) => roomActions.invite(roomId, id, reason));
|
||||
},
|
||||
},
|
||||
disinvite: {
|
||||
name: 'disinvite',
|
||||
description: 'Disinvite user to room. Example: /disinvite userId1 userId2 [-r reason]',
|
||||
exe: (roomId, data) => {
|
||||
const { userIds, reason } = processMxidAndReason(data);
|
||||
userIds.map((id) => roomActions.kick(roomId, id, reason));
|
||||
},
|
||||
},
|
||||
kick: {
|
||||
name: 'kick',
|
||||
description: 'Kick user from room. Example: /kick userId1 userId2 [-r reason]',
|
||||
exe: (roomId, data) => {
|
||||
const { userIds, reason } = processMxidAndReason(data);
|
||||
userIds.map((id) => roomActions.kick(roomId, id, reason));
|
||||
},
|
||||
},
|
||||
ban: {
|
||||
name: 'ban',
|
||||
description: 'Ban user from room. Example: /ban userId1 userId2 [-r reason]',
|
||||
exe: (roomId, data) => {
|
||||
const { userIds, reason } = processMxidAndReason(data);
|
||||
userIds.map((id) => roomActions.ban(roomId, id, reason));
|
||||
},
|
||||
},
|
||||
unban: {
|
||||
name: 'unban',
|
||||
description: 'Unban user from room. Example: /unban userId1 userId2',
|
||||
exe: (roomId, data) => {
|
||||
const rawIds = data.split(' ');
|
||||
const userIds = rawIds.filter((id) => id.match(MXID_REG));
|
||||
userIds.map((id) => roomActions.unban(roomId, id));
|
||||
},
|
||||
},
|
||||
ignore: {
|
||||
name: 'ignore',
|
||||
description: 'Ignore user. Example: /ignore userId1 userId2',
|
||||
exe: (roomId, data) => {
|
||||
const rawIds = data.split(' ');
|
||||
const userIds = rawIds.filter((id) => id.match(MXID_REG));
|
||||
if (userIds.length > 0) roomActions.ignore(userIds);
|
||||
},
|
||||
},
|
||||
unignore: {
|
||||
name: 'unignore',
|
||||
description: 'Unignore user. Example: /unignore userId1 userId2',
|
||||
exe: (roomId, data) => {
|
||||
const rawIds = data.split(' ');
|
||||
const userIds = rawIds.filter((id) => id.match(MXID_REG));
|
||||
if (userIds.length > 0) roomActions.unignore(userIds);
|
||||
},
|
||||
},
|
||||
myroomnick: {
|
||||
name: 'myroomnick',
|
||||
description: 'Change nick in current room.',
|
||||
exe: (roomId, data) => {
|
||||
const nick = data.trim();
|
||||
if (nick === '') return;
|
||||
roomActions.setMyRoomNick(roomId, nick);
|
||||
},
|
||||
},
|
||||
myroomavatar: {
|
||||
name: 'myroomavatar',
|
||||
description: 'Change profile picture in current room. Example /myroomavatar mxc://xyzabc',
|
||||
exe: (roomId, data) => {
|
||||
if (data.match(MXC_REG)) {
|
||||
roomActions.setMyRoomAvatar(roomId, data);
|
||||
}
|
||||
},
|
||||
},
|
||||
converttodm: {
|
||||
name: 'converttodm',
|
||||
description: 'Convert room to direct message',
|
||||
exe: (roomId) => {
|
||||
roomActions.convertToDm(roomId);
|
||||
},
|
||||
},
|
||||
converttoroom: {
|
||||
name: 'converttoroom',
|
||||
description: 'Convert direct message to room',
|
||||
exe: (roomId) => {
|
||||
roomActions.convertToRoom(roomId);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function openHelpDialog() {
|
||||
openReusableDialog(
|
||||
<Text variant="s1" weight="medium">Commands</Text>,
|
||||
() => (
|
||||
<div className="commands-dialog">
|
||||
{Object.keys(commands).map((cmdName) => (
|
||||
<SettingTile
|
||||
key={cmdName}
|
||||
title={cmdName}
|
||||
content={<Text variant="b3">{commands[cmdName].description}</Text>}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export default commands;
|
||||
@@ -1,10 +0,0 @@
|
||||
.commands-dialog {
|
||||
& > * {
|
||||
padding: var(--sp-tight) var(--sp-normal);
|
||||
border-bottom: 1px solid var(--bg-surface-border);
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: var(--sp-extra-loose);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { twemojify } from '../../../util/twemojify';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil';
|
||||
|
||||
function getTimelineJSXMessages() {
|
||||
return {
|
||||
join(user) {
|
||||
return (
|
||||
<>
|
||||
<b>{twemojify(user)}</b>
|
||||
{' joined the room'}
|
||||
</>
|
||||
);
|
||||
},
|
||||
leave(user, reason) {
|
||||
const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
|
||||
return (
|
||||
<>
|
||||
<b>{twemojify(user)}</b>
|
||||
{' left the room'}
|
||||
{twemojify(reasonMsg)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
invite(inviter, user) {
|
||||
return (
|
||||
<>
|
||||
<b>{twemojify(inviter)}</b>
|
||||
{' invited '}
|
||||
<b>{twemojify(user)}</b>
|
||||
</>
|
||||
);
|
||||
},
|
||||
cancelInvite(inviter, user) {
|
||||
return (
|
||||
<>
|
||||
<b>{twemojify(inviter)}</b>
|
||||
{' canceled '}
|
||||
<b>{twemojify(user)}</b>
|
||||
{'\'s invite'}
|
||||
</>
|
||||
);
|
||||
},
|
||||
rejectInvite(user) {
|
||||
return (
|
||||
<>
|
||||
<b>{twemojify(user)}</b>
|
||||
{' rejected the invitation'}
|
||||
</>
|
||||
);
|
||||
},
|
||||
kick(actor, user, reason) {
|
||||
const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
|
||||
return (
|
||||
<>
|
||||
<b>{twemojify(actor)}</b>
|
||||
{' kicked '}
|
||||
<b>{twemojify(user)}</b>
|
||||
{twemojify(reasonMsg)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
ban(actor, user, reason) {
|
||||
const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
|
||||
return (
|
||||
<>
|
||||
<b>{twemojify(actor)}</b>
|
||||
{' banned '}
|
||||
<b>{twemojify(user)}</b>
|
||||
{twemojify(reasonMsg)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
unban(actor, user) {
|
||||
return (
|
||||
<>
|
||||
<b>{twemojify(actor)}</b>
|
||||
{' unbanned '}
|
||||
<b>{twemojify(user)}</b>
|
||||
</>
|
||||
);
|
||||
},
|
||||
avatarSets(user) {
|
||||
return (
|
||||
<>
|
||||
<b>{twemojify(user)}</b>
|
||||
{' set a avatar'}
|
||||
</>
|
||||
);
|
||||
},
|
||||
avatarChanged(user) {
|
||||
return (
|
||||
<>
|
||||
<b>{twemojify(user)}</b>
|
||||
{' changed their avatar'}
|
||||
</>
|
||||
);
|
||||
},
|
||||
avatarRemoved(user) {
|
||||
return (
|
||||
<>
|
||||
<b>{twemojify(user)}</b>
|
||||
{' removed their avatar'}
|
||||
</>
|
||||
);
|
||||
},
|
||||
nameSets(user, newName) {
|
||||
return (
|
||||
<>
|
||||
<b>{twemojify(user)}</b>
|
||||
{' set display name to '}
|
||||
<b>{twemojify(newName)}</b>
|
||||
</>
|
||||
);
|
||||
},
|
||||
nameChanged(user, newName) {
|
||||
return (
|
||||
<>
|
||||
<b>{twemojify(user)}</b>
|
||||
{' changed their display name to '}
|
||||
<b>{twemojify(newName)}</b>
|
||||
</>
|
||||
);
|
||||
},
|
||||
nameRemoved(user, lastName) {
|
||||
return (
|
||||
<>
|
||||
<b>{twemojify(user)}</b>
|
||||
{' removed their display name '}
|
||||
<b>{twemojify(lastName)}</b>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getUsersActionJsx(roomId, userIds, actionStr) {
|
||||
const room = initMatrix.matrixClient.getRoom(roomId);
|
||||
const getUserDisplayName = (userId) => {
|
||||
if (room?.getMember(userId)) return getUsernameOfRoomMember(room.getMember(userId));
|
||||
return getUsername(userId);
|
||||
};
|
||||
const getUserJSX = (userId) => <b>{twemojify(getUserDisplayName(userId))}</b>;
|
||||
if (!Array.isArray(userIds)) return 'Idle';
|
||||
if (userIds.length === 0) return 'Idle';
|
||||
const MAX_VISIBLE_COUNT = 3;
|
||||
|
||||
const u1Jsx = getUserJSX(userIds[0]);
|
||||
// eslint-disable-next-line react/jsx-one-expression-per-line
|
||||
if (userIds.length === 1) return <>{u1Jsx} is {actionStr}</>;
|
||||
|
||||
const u2Jsx = getUserJSX(userIds[1]);
|
||||
// eslint-disable-next-line react/jsx-one-expression-per-line
|
||||
if (userIds.length === 2) return <>{u1Jsx} and {u2Jsx} are {actionStr}</>;
|
||||
|
||||
const u3Jsx = getUserJSX(userIds[2]);
|
||||
if (userIds.length === 3) {
|
||||
// eslint-disable-next-line react/jsx-one-expression-per-line
|
||||
return <>{u1Jsx}, {u2Jsx} and {u3Jsx} are {actionStr}</>;
|
||||
}
|
||||
|
||||
const othersCount = userIds.length - MAX_VISIBLE_COUNT;
|
||||
// eslint-disable-next-line react/jsx-one-expression-per-line
|
||||
return <>{u1Jsx}, {u2Jsx}, {u3Jsx} and {othersCount} others are {actionStr}</>;
|
||||
}
|
||||
|
||||
function parseTimelineChange(mEvent) {
|
||||
const tJSXMsgs = getTimelineJSXMessages();
|
||||
const makeReturnObj = (variant, content) => ({
|
||||
variant,
|
||||
content,
|
||||
});
|
||||
const content = mEvent.getContent();
|
||||
const prevContent = mEvent.getPrevContent();
|
||||
const sender = mEvent.getSender();
|
||||
const senderName = getUsername(sender);
|
||||
const userName = getUsername(mEvent.getStateKey());
|
||||
|
||||
switch (content.membership) {
|
||||
case 'invite': return makeReturnObj('invite', tJSXMsgs.invite(senderName, userName));
|
||||
case 'ban': return makeReturnObj('leave', tJSXMsgs.ban(senderName, userName, content.reason));
|
||||
case 'join':
|
||||
if (prevContent.membership === 'join') {
|
||||
if (content.displayname !== prevContent.displayname) {
|
||||
if (typeof content.displayname === 'undefined') return makeReturnObj('avatar', tJSXMsgs.nameRemoved(sender, prevContent.displayname));
|
||||
if (typeof prevContent.displayname === 'undefined') return makeReturnObj('avatar', tJSXMsgs.nameSets(sender, content.displayname));
|
||||
return makeReturnObj('avatar', tJSXMsgs.nameChanged(prevContent.displayname, content.displayname));
|
||||
}
|
||||
if (content.avatar_url !== prevContent.avatar_url) {
|
||||
if (typeof content.avatar_url === 'undefined') return makeReturnObj('avatar', tJSXMsgs.avatarRemoved(content.displayname));
|
||||
if (typeof prevContent.avatar_url === 'undefined') return makeReturnObj('avatar', tJSXMsgs.avatarSets(content.displayname));
|
||||
return makeReturnObj('avatar', tJSXMsgs.avatarChanged(content.displayname));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return makeReturnObj('join', tJSXMsgs.join(senderName));
|
||||
case 'leave':
|
||||
if (sender === mEvent.getStateKey()) {
|
||||
switch (prevContent.membership) {
|
||||
case 'invite': return makeReturnObj('invite-cancel', tJSXMsgs.rejectInvite(senderName));
|
||||
default: return makeReturnObj('leave', tJSXMsgs.leave(senderName, content.reason));
|
||||
}
|
||||
}
|
||||
switch (prevContent.membership) {
|
||||
case 'invite': return makeReturnObj('invite-cancel', tJSXMsgs.cancelInvite(senderName, userName));
|
||||
case 'ban': return makeReturnObj('other', tJSXMsgs.unban(senderName, userName));
|
||||
// sender is not target and made the target leave,
|
||||
// if not from invite/ban then this is a kick
|
||||
default: return makeReturnObj('leave', tJSXMsgs.kick(senderName, userName, content.reason));
|
||||
}
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
getTimelineJSXMessages,
|
||||
getUsersActionJsx,
|
||||
parseTimelineChange,
|
||||
};
|
||||
Reference in New Issue
Block a user