Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
parent
0c0a978886
commit
38cbb87a62
21 changed files with 948 additions and 507 deletions
8
src/app/hooks/useForceUpdate.js
Normal file
8
src/app/hooks/useForceUpdate.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export function useForceUpdate() {
|
||||||
|
const [, setData] = useState(null);
|
||||||
|
|
||||||
|
return () => setData({});
|
||||||
|
}
|
22
src/app/hooks/useStore.js
Normal file
22
src/app/hooks/useStore.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export function useStore(...args) {
|
||||||
|
const itemRef = useRef(null);
|
||||||
|
|
||||||
|
const getItem = () => itemRef.current;
|
||||||
|
|
||||||
|
const setItem = (event) => {
|
||||||
|
itemRef.current = event;
|
||||||
|
return itemRef.current;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
itemRef.current = null;
|
||||||
|
return () => {
|
||||||
|
itemRef.current = null;
|
||||||
|
};
|
||||||
|
}, args);
|
||||||
|
|
||||||
|
return { getItem, setItem };
|
||||||
|
}
|
|
@ -95,7 +95,7 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
|
||||||
const [isInvited, setIsInvited] = useState(member?.membership === 'invite');
|
const [isInvited, setIsInvited] = useState(member?.membership === 'invite');
|
||||||
|
|
||||||
const myPowerlevel = room.getMember(mx.getUserId()).powerLevel;
|
const myPowerlevel = room.getMember(mx.getUserId()).powerLevel;
|
||||||
const userPL = room.getMember(userId).powerLevel || 0;
|
const userPL = room.getMember(userId)?.powerLevel || 0;
|
||||||
const canIKick = room.currentState.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel;
|
const canIKick = room.currentState.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel;
|
||||||
|
|
||||||
const onCreated = (dmRoomId) => {
|
const onCreated = (dmRoomId) => {
|
||||||
|
|
|
@ -15,27 +15,15 @@ import { openProfileViewer } from '../../../client/action/navigation';
|
||||||
|
|
||||||
function ReadReceipts() {
|
function ReadReceipts() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [readers, setReaders] = useState([]);
|
||||||
const [roomId, setRoomId] = useState(null);
|
const [roomId, setRoomId] = useState(null);
|
||||||
const [readReceipts, setReadReceipts] = useState([]);
|
|
||||||
|
|
||||||
function loadReadReceipts(myRoomId, eventId) {
|
|
||||||
const mx = initMatrix.matrixClient;
|
|
||||||
const room = mx.getRoom(myRoomId);
|
|
||||||
const { timeline } = room;
|
|
||||||
const myReadReceipts = [];
|
|
||||||
|
|
||||||
const myEventIndex = timeline.findIndex((mEvent) => mEvent.getId() === eventId);
|
|
||||||
|
|
||||||
for (let eventIndex = myEventIndex; eventIndex < timeline.length; eventIndex += 1) {
|
|
||||||
myReadReceipts.push(...room.getReceiptsForEvent(timeline[eventIndex]));
|
|
||||||
}
|
|
||||||
|
|
||||||
setReadReceipts(myReadReceipts);
|
|
||||||
setRoomId(myRoomId);
|
|
||||||
setIsOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const loadReadReceipts = (rId, userIds) => {
|
||||||
|
setReaders(userIds);
|
||||||
|
setRoomId(rId);
|
||||||
|
setIsOpen(true);
|
||||||
|
};
|
||||||
navigation.on(cons.events.navigation.READRECEIPTS_OPENED, loadReadReceipts);
|
navigation.on(cons.events.navigation.READRECEIPTS_OPENED, loadReadReceipts);
|
||||||
return () => {
|
return () => {
|
||||||
navigation.removeListener(cons.events.navigation.READRECEIPTS_OPENED, loadReadReceipts);
|
navigation.removeListener(cons.events.navigation.READRECEIPTS_OPENED, loadReadReceipts);
|
||||||
|
@ -44,28 +32,28 @@ function ReadReceipts() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen === false) {
|
if (isOpen === false) {
|
||||||
|
setReaders([]);
|
||||||
setRoomId(null);
|
setRoomId(null);
|
||||||
setReadReceipts([]);
|
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
function renderPeople(receipt) {
|
function renderPeople(userId) {
|
||||||
const room = initMatrix.matrixClient.getRoom(roomId);
|
const room = initMatrix.matrixClient.getRoom(roomId);
|
||||||
const member = room.getMember(receipt.userId);
|
const member = room.getMember(userId);
|
||||||
const getUserDisplayName = (userId) => {
|
const getUserDisplayName = () => {
|
||||||
if (room?.getMember(userId)) return getUsernameOfRoomMember(room.getMember(userId));
|
if (room?.getMember(userId)) return getUsernameOfRoomMember(room.getMember(userId));
|
||||||
return getUsername(userId);
|
return getUsername(userId);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<PeopleSelector
|
<PeopleSelector
|
||||||
key={receipt.userId}
|
key={userId}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
openProfileViewer(receipt.userId, roomId);
|
openProfileViewer(userId, roomId);
|
||||||
}}
|
}}
|
||||||
avatarSrc={member?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 24, 24, 'crop')}
|
avatarSrc={member?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 24, 24, 'crop')}
|
||||||
name={getUserDisplayName(receipt.userId)}
|
name={getUserDisplayName(userId)}
|
||||||
color={colorMXID(receipt.userId)}
|
color={colorMXID(userId)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -78,7 +66,7 @@ function ReadReceipts() {
|
||||||
contentOptions={<IconButton src={CrossIC} onClick={() => setIsOpen(false)} tooltip="Close" />}
|
contentOptions={<IconButton src={CrossIC} onClick={() => setIsOpen(false)} tooltip="Close" />}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
readReceipts.map(renderPeople)
|
readers.map(renderPeople)
|
||||||
}
|
}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,39 +1,50 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import './Room.scss';
|
import './Room.scss';
|
||||||
|
|
||||||
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
import settings from '../../../client/state/settings';
|
import settings from '../../../client/state/settings';
|
||||||
|
import RoomTimeline from '../../../client/state/RoomTimeline';
|
||||||
|
|
||||||
import Welcome from '../welcome/Welcome';
|
import Welcome from '../welcome/Welcome';
|
||||||
import RoomView from './RoomView';
|
import RoomView from './RoomView';
|
||||||
import PeopleDrawer from './PeopleDrawer';
|
import PeopleDrawer from './PeopleDrawer';
|
||||||
|
|
||||||
function Room() {
|
function Room() {
|
||||||
const [selectedRoomId, changeSelectedRoomId] = useState(null);
|
const [roomTimeline, setRoomTimeline] = useState(null);
|
||||||
const [isDrawerVisible, toggleDrawerVisiblity] = useState(settings.isPeopleDrawer);
|
const [eventId, setEventId] = useState(null);
|
||||||
|
const [isDrawer, setIsDrawer] = useState(settings.isPeopleDrawer);
|
||||||
|
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
const handleRoomSelected = (rId, pRoomId, eId) => {
|
||||||
|
if (mx.getRoom(rId)) {
|
||||||
|
setRoomTimeline(new RoomTimeline(rId));
|
||||||
|
setEventId(eId);
|
||||||
|
} else {
|
||||||
|
// TODO: add ability to join room if roomId is invalid
|
||||||
|
setRoomTimeline(null);
|
||||||
|
setEventId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleDrawerToggling = (visiblity) => setIsDrawer(visiblity);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRoomSelected = (roomId) => {
|
|
||||||
changeSelectedRoomId(roomId);
|
|
||||||
};
|
|
||||||
const handleDrawerToggling = (visiblity) => {
|
|
||||||
toggleDrawerVisiblity(visiblity);
|
|
||||||
};
|
|
||||||
navigation.on(cons.events.navigation.ROOM_SELECTED, handleRoomSelected);
|
navigation.on(cons.events.navigation.ROOM_SELECTED, handleRoomSelected);
|
||||||
settings.on(cons.events.settings.PEOPLE_DRAWER_TOGGLED, handleDrawerToggling);
|
settings.on(cons.events.settings.PEOPLE_DRAWER_TOGGLED, handleDrawerToggling);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, handleRoomSelected);
|
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, handleRoomSelected);
|
||||||
settings.removeListener(cons.events.settings.PEOPLE_DRAWER_TOGGLED, handleDrawerToggling);
|
settings.removeListener(cons.events.settings.PEOPLE_DRAWER_TOGGLED, handleDrawerToggling);
|
||||||
|
roomTimeline?.removeInternalListeners();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (selectedRoomId === null) return <Welcome />;
|
if (roomTimeline === null) return <Welcome />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="room-container">
|
<div className="room-container">
|
||||||
<RoomView roomId={selectedRoomId} />
|
<RoomView roomTimeline={roomTimeline} eventId={eventId} />
|
||||||
{ isDrawerVisible && <PeopleDrawer roomId={selectedRoomId} />}
|
{ isDrawer && <PeopleDrawer roomId={roomTimeline.roomId} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,9 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './RoomView.scss';
|
import './RoomView.scss';
|
||||||
|
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
|
|
||||||
import RoomTimeline from '../../../client/state/RoomTimeline';
|
|
||||||
import { Debounce, getScrollInfo } from '../../../util/common';
|
|
||||||
|
|
||||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
|
||||||
|
|
||||||
import RoomViewHeader from './RoomViewHeader';
|
import RoomViewHeader from './RoomViewHeader';
|
||||||
import RoomViewContent from './RoomViewContent';
|
import RoomViewContent from './RoomViewContent';
|
||||||
import RoomViewFloating from './RoomViewFloating';
|
import RoomViewFloating from './RoomViewFloating';
|
||||||
|
@ -17,146 +12,31 @@ import RoomViewCmdBar from './RoomViewCmdBar';
|
||||||
|
|
||||||
const viewEvent = new EventEmitter();
|
const viewEvent = new EventEmitter();
|
||||||
|
|
||||||
function RoomView({ roomId }) {
|
function RoomView({ roomTimeline, eventId }) {
|
||||||
const [roomTimeline, updateRoomTimeline] = useState(null);
|
// eslint-disable-next-line react/prop-types
|
||||||
const [debounce] = useState(new Debounce());
|
const { roomId } = roomTimeline;
|
||||||
const timelineSVRef = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
roomTimeline?.removeInternalListeners();
|
|
||||||
updateRoomTimeline(new RoomTimeline(roomId));
|
|
||||||
}, [roomId]);
|
|
||||||
|
|
||||||
const timelineScroll = {
|
|
||||||
reachBottom() {
|
|
||||||
timelineScroll.isOngoing = true;
|
|
||||||
const target = timelineSVRef?.current;
|
|
||||||
if (!target) return;
|
|
||||||
const maxScrollTop = target.scrollHeight - target.offsetHeight;
|
|
||||||
target.scrollTop = maxScrollTop;
|
|
||||||
timelineScroll.position = 'BOTTOM';
|
|
||||||
timelineScroll.isScrollable = maxScrollTop > 0;
|
|
||||||
timelineScroll.isInTopHalf = false;
|
|
||||||
timelineScroll.lastTopMsg = null;
|
|
||||||
timelineScroll.lastBottomMsg = null;
|
|
||||||
},
|
|
||||||
autoReachBottom() {
|
|
||||||
if (timelineScroll.position === 'BOTTOM') timelineScroll.reachBottom();
|
|
||||||
},
|
|
||||||
tryRestoringScroll() {
|
|
||||||
timelineScroll.isOngoing = true;
|
|
||||||
const sv = timelineSVRef.current;
|
|
||||||
const {
|
|
||||||
lastTopMsg, lastBottomMsg,
|
|
||||||
diff, isInTopHalf, lastTop,
|
|
||||||
} = timelineScroll;
|
|
||||||
|
|
||||||
if (lastTopMsg === null) {
|
|
||||||
sv.scrollTop = sv.scrollHeight;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ot = isInTopHalf ? lastTopMsg?.offsetTop : lastBottomMsg?.offsetTop;
|
|
||||||
if (!ot) sv.scrollTop = lastTop;
|
|
||||||
else sv.scrollTop = ot - diff;
|
|
||||||
},
|
|
||||||
position: 'BOTTOM',
|
|
||||||
isScrollable: false,
|
|
||||||
isInTopHalf: false,
|
|
||||||
maxEvents: 50,
|
|
||||||
lastTop: 0,
|
|
||||||
lastHeight: 0,
|
|
||||||
lastViewHeight: 0,
|
|
||||||
lastTopMsg: null,
|
|
||||||
lastBottomMsg: null,
|
|
||||||
diff: 0,
|
|
||||||
isOngoing: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const calcScroll = (target) => {
|
|
||||||
if (timelineScroll.isOngoing) {
|
|
||||||
timelineScroll.isOngoing = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const PLACEHOLDER_COUNT = 2;
|
|
||||||
const PLACEHOLDER_HEIGHT = 96 * PLACEHOLDER_COUNT;
|
|
||||||
const SMALLEST_MSG_HEIGHT = 32;
|
|
||||||
const scroll = getScrollInfo(target);
|
|
||||||
|
|
||||||
const isPaginateBack = scroll.top < PLACEHOLDER_HEIGHT;
|
|
||||||
const isPaginateForward = scroll.bottom > (scroll.height - PLACEHOLDER_HEIGHT);
|
|
||||||
timelineScroll.isInTopHalf = scroll.top + (scroll.viewHeight / 2) < scroll.height / 2;
|
|
||||||
|
|
||||||
if (timelineScroll.lastViewHeight !== scroll.viewHeight) {
|
|
||||||
timelineScroll.maxEvents = Math.round(scroll.viewHeight / SMALLEST_MSG_HEIGHT) * 3;
|
|
||||||
timelineScroll.lastViewHeight = scroll.viewHeight;
|
|
||||||
}
|
|
||||||
timelineScroll.isScrollable = scroll.isScrollable;
|
|
||||||
timelineScroll.lastTop = scroll.top;
|
|
||||||
timelineScroll.lastHeight = scroll.height;
|
|
||||||
const tChildren = target.lastElementChild.lastElementChild.children;
|
|
||||||
const lCIndex = tChildren.length - 1;
|
|
||||||
|
|
||||||
timelineScroll.lastTopMsg = tChildren[0]?.className === 'ph-msg'
|
|
||||||
? tChildren[PLACEHOLDER_COUNT]
|
|
||||||
: tChildren[0];
|
|
||||||
timelineScroll.lastBottomMsg = tChildren[lCIndex]?.className === 'ph-msg'
|
|
||||||
? tChildren[lCIndex - PLACEHOLDER_COUNT]
|
|
||||||
: tChildren[lCIndex];
|
|
||||||
|
|
||||||
if (timelineScroll.isInTopHalf && timelineScroll.lastBottomMsg) {
|
|
||||||
timelineScroll.diff = timelineScroll.lastTopMsg.offsetTop - scroll.top;
|
|
||||||
} else {
|
|
||||||
timelineScroll.diff = timelineScroll.lastBottomMsg.offsetTop - scroll.top;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPaginateBack) {
|
|
||||||
timelineScroll.position = 'TOP';
|
|
||||||
viewEvent.emit('timeline-scroll', timelineScroll.position);
|
|
||||||
} else if (isPaginateForward) {
|
|
||||||
timelineScroll.position = 'BOTTOM';
|
|
||||||
viewEvent.emit('timeline-scroll', timelineScroll.position);
|
|
||||||
} else {
|
|
||||||
timelineScroll.position = 'BETWEEN';
|
|
||||||
viewEvent.emit('timeline-scroll', timelineScroll.position);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTimelineScroll = (event) => {
|
|
||||||
const { target } = event;
|
|
||||||
if (!target) return;
|
|
||||||
debounce._(calcScroll, 200)(target);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
console.log('----roomId changed');
|
||||||
return (
|
return (
|
||||||
<div className="room-view">
|
<div className="room-view">
|
||||||
<RoomViewHeader roomId={roomId} />
|
<RoomViewHeader roomId={roomId} />
|
||||||
<div className="room-view__content-wrapper">
|
<div className="room-view__content-wrapper">
|
||||||
<div className="room-view__scrollable">
|
<div className="room-view__scrollable">
|
||||||
<ScrollView onScroll={handleTimelineScroll} ref={timelineSVRef} autoHide>
|
|
||||||
{roomTimeline !== null && (
|
|
||||||
<RoomViewContent
|
<RoomViewContent
|
||||||
roomId={roomId}
|
eventId={eventId}
|
||||||
roomTimeline={roomTimeline}
|
roomTimeline={roomTimeline}
|
||||||
timelineScroll={timelineScroll}
|
|
||||||
viewEvent={viewEvent}
|
viewEvent={viewEvent}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
{roomTimeline !== null && (
|
|
||||||
<RoomViewFloating
|
<RoomViewFloating
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
roomTimeline={roomTimeline}
|
roomTimeline={roomTimeline}
|
||||||
viewEvent={viewEvent}
|
viewEvent={viewEvent}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{roomTimeline !== null && (
|
|
||||||
<div className="room-view__sticky">
|
<div className="room-view__sticky">
|
||||||
<RoomViewInput
|
<RoomViewInput
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
roomTimeline={roomTimeline}
|
roomTimeline={roomTimeline}
|
||||||
timelineScroll={timelineScroll}
|
|
||||||
viewEvent={viewEvent}
|
viewEvent={viewEvent}
|
||||||
/>
|
/>
|
||||||
<RoomViewCmdBar
|
<RoomViewCmdBar
|
||||||
|
@ -165,13 +45,17 @@ function RoomView({ roomId }) {
|
||||||
viewEvent={viewEvent}
|
viewEvent={viewEvent}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RoomView.defaultProps = {
|
||||||
|
eventId: null,
|
||||||
|
};
|
||||||
RoomView.propTypes = {
|
RoomView.propTypes = {
|
||||||
roomId: PropTypes.string.isRequired,
|
roomTimeline: PropTypes.shape({}).isRequired,
|
||||||
|
eventId: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RoomView;
|
export default RoomView;
|
||||||
|
|
|
@ -123,37 +123,29 @@ function FollowingMembers({ roomId, roomTimeline, viewEvent }) {
|
||||||
const [followingMembers, setFollowingMembers] = useState([]);
|
const [followingMembers, setFollowingMembers] = useState([]);
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
|
|
||||||
function handleOnMessageSent() {
|
const handleOnMessageSent = () => setFollowingMembers([]);
|
||||||
setFollowingMembers([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFollowingMembers() {
|
const updateFollowingMembers = () => {
|
||||||
const room = mx.getRoom(roomId);
|
|
||||||
const { timeline } = room;
|
|
||||||
const userIds = room.getUsersReadUpTo(timeline[timeline.length - 1]);
|
|
||||||
const myUserId = mx.getUserId();
|
const myUserId = mx.getUserId();
|
||||||
setFollowingMembers(userIds.filter((userId) => userId !== myUserId));
|
setFollowingMembers(roomTimeline.getLiveReaders().filter((userId) => userId !== myUserId));
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => updateFollowingMembers(), [roomId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
roomTimeline.on(cons.events.roomTimeline.READ_RECEIPT, updateFollowingMembers);
|
updateFollowingMembers();
|
||||||
|
roomTimeline.on(cons.events.roomTimeline.LIVE_RECEIPT, updateFollowingMembers);
|
||||||
viewEvent.on('message_sent', handleOnMessageSent);
|
viewEvent.on('message_sent', handleOnMessageSent);
|
||||||
return () => {
|
return () => {
|
||||||
roomTimeline.removeListener(cons.events.roomTimeline.READ_RECEIPT, updateFollowingMembers);
|
roomTimeline.removeListener(cons.events.roomTimeline.LIVE_RECEIPT, updateFollowingMembers);
|
||||||
viewEvent.removeListener('message_sent', handleOnMessageSent);
|
viewEvent.removeListener('message_sent', handleOnMessageSent);
|
||||||
};
|
};
|
||||||
}, [roomTimeline]);
|
}, [roomTimeline]);
|
||||||
|
|
||||||
const { timeline } = roomTimeline.room;
|
|
||||||
const lastMEvent = timeline[timeline.length - 1];
|
|
||||||
return followingMembers.length !== 0 && (
|
return followingMembers.length !== 0 && (
|
||||||
<TimelineChange
|
<TimelineChange
|
||||||
variant="follow"
|
variant="follow"
|
||||||
content={getUsersActionJsx(roomId, followingMembers, 'following the conversation.')}
|
content={getUsersActionJsx(roomId, followingMembers, 'following the conversation.')}
|
||||||
time=""
|
time=""
|
||||||
onClick={() => openReadReceipts(roomId, lastMEvent.getId())}
|
onClick={() => openReadReceipts(roomId, followingMembers)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,52 @@
|
||||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
/* eslint-disable react/prop-types */
|
/* eslint-disable react/prop-types */
|
||||||
import React, { useState, useEffect, useLayoutEffect } from 'react';
|
import React, {
|
||||||
|
useState, useEffect, useLayoutEffect, useCallback, useRef,
|
||||||
|
} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './RoomViewContent.scss';
|
import './RoomViewContent.scss';
|
||||||
|
|
||||||
|
import EventEmitter from 'events';
|
||||||
import dateFormat from 'dateformat';
|
import dateFormat from 'dateformat';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import { diffMinutes, isNotInSameDay } from '../../../util/common';
|
import navigation from '../../../client/state/navigation';
|
||||||
import { openProfileViewer } from '../../../client/action/navigation';
|
import { openProfileViewer } from '../../../client/action/navigation';
|
||||||
|
import {
|
||||||
|
diffMinutes, isNotInSameDay, Throttle, getScrollInfo,
|
||||||
|
} from '../../../util/common';
|
||||||
|
|
||||||
import Divider from '../../atoms/divider/Divider';
|
import Divider from '../../atoms/divider/Divider';
|
||||||
|
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||||
import { Message, PlaceholderMessage } from '../../molecules/message/Message';
|
import { Message, PlaceholderMessage } from '../../molecules/message/Message';
|
||||||
import RoomIntro from '../../molecules/room-intro/RoomIntro';
|
import RoomIntro from '../../molecules/room-intro/RoomIntro';
|
||||||
import TimelineChange from '../../molecules/message/TimelineChange';
|
import TimelineChange from '../../molecules/message/TimelineChange';
|
||||||
|
|
||||||
|
import { useStore } from '../../hooks/useStore';
|
||||||
import { parseTimelineChange } from './common';
|
import { parseTimelineChange } from './common';
|
||||||
|
|
||||||
const MAX_MSG_DIFF_MINUTES = 5;
|
const MAX_MSG_DIFF_MINUTES = 5;
|
||||||
|
const PLACEHOLDER_COUNT = 2;
|
||||||
|
const PLACEHOLDERS_HEIGHT = 96 * PLACEHOLDER_COUNT;
|
||||||
|
const SCROLL_TRIGGER_POS = PLACEHOLDERS_HEIGHT * 4;
|
||||||
|
|
||||||
|
const SMALLEST_MSG_HEIGHT = 32;
|
||||||
|
const PAGES_COUNT = 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;
|
||||||
|
};
|
||||||
|
|
||||||
function genPlaceholders(key) {
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={`placeholder-container${key}`}>
|
<React.Fragment key={`placeholder-container${key}`}>
|
||||||
<PlaceholderMessage key={`placeholder-1${key}`} />
|
{genPlaceholders()}
|
||||||
<PlaceholderMessage key={`placeholder-2${key}`} />
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -49,228 +70,421 @@ function genRoomIntro(mEvent, roomTimeline) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const scroll = {
|
function handleOnClickCapture(e) {
|
||||||
from: 0,
|
|
||||||
limit: 0,
|
|
||||||
getEndIndex() {
|
|
||||||
return (this.from + this.limit);
|
|
||||||
},
|
|
||||||
isNewEvent: false,
|
|
||||||
};
|
|
||||||
function RoomViewContent({
|
|
||||||
roomId, roomTimeline, timelineScroll, viewEvent,
|
|
||||||
}) {
|
|
||||||
const [isReachedTimelineEnd, setIsReachedTimelineEnd] = useState(false);
|
|
||||||
const [onStateUpdate, updateState] = useState(null);
|
|
||||||
|
|
||||||
const mx = initMatrix.matrixClient;
|
|
||||||
const noti = initMatrix.notifications;
|
|
||||||
|
|
||||||
if (scroll.limit === 0) {
|
|
||||||
const from = roomTimeline.timeline.size - timelineScroll.maxEvents;
|
|
||||||
scroll.from = (from < 0) ? 0 : from;
|
|
||||||
scroll.limit = timelineScroll.maxEvents;
|
|
||||||
}
|
|
||||||
|
|
||||||
function autoLoadTimeline() {
|
|
||||||
if (timelineScroll.isScrollable === true) return;
|
|
||||||
roomTimeline.paginateBack();
|
|
||||||
}
|
|
||||||
function trySendingReadReceipt() {
|
|
||||||
const { timeline } = roomTimeline.room;
|
|
||||||
if (
|
|
||||||
(noti.doesRoomHaveUnread(roomTimeline.room) || noti.hasNoti(roomId))
|
|
||||||
&& timeline.length !== 0) {
|
|
||||||
mx.sendReadReceipt(timeline[timeline.length - 1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getNewFrom = (position) => {
|
|
||||||
let newFrom = scroll.from;
|
|
||||||
const tSize = roomTimeline.timeline.size;
|
|
||||||
const doPaginate = tSize > timelineScroll.maxEvents;
|
|
||||||
if (!doPaginate || scroll.from < 0) newFrom = 0;
|
|
||||||
const newEventCount = Math.round(timelineScroll.maxEvents / 2);
|
|
||||||
scroll.limit = timelineScroll.maxEvents;
|
|
||||||
|
|
||||||
if (position === 'TOP' && doPaginate) newFrom -= newEventCount;
|
|
||||||
if (position === 'BOTTOM' && doPaginate) newFrom += newEventCount;
|
|
||||||
|
|
||||||
if (newFrom >= tSize || scroll.getEndIndex() >= tSize) newFrom = tSize - scroll.limit - 1;
|
|
||||||
if (newFrom < 0) newFrom = 0;
|
|
||||||
return newFrom;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTimelineScroll = (position) => {
|
|
||||||
const tSize = roomTimeline.timeline.size;
|
|
||||||
if (position === 'BETWEEN') return;
|
|
||||||
if (position === 'BOTTOM' && scroll.getEndIndex() + 1 === tSize) return;
|
|
||||||
|
|
||||||
if (scroll.from === 0 && position === 'TOP') {
|
|
||||||
// Fetch back history.
|
|
||||||
if (roomTimeline.isOngoingPagination || isReachedTimelineEnd) return;
|
|
||||||
roomTimeline.paginateBack();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
scroll.from = getNewFrom(position);
|
|
||||||
updateState({});
|
|
||||||
|
|
||||||
if (scroll.getEndIndex() + 1 >= tSize) {
|
|
||||||
trySendingReadReceipt();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatePAG = (canPagMore, loaded) => {
|
|
||||||
if (canPagMore) {
|
|
||||||
scroll.from += loaded;
|
|
||||||
scroll.from = getNewFrom(timelineScroll.position);
|
|
||||||
if (roomTimeline.ongoingDecryptionCount === 0) updateState({});
|
|
||||||
} else setIsReachedTimelineEnd(true);
|
|
||||||
};
|
|
||||||
// force update RoomTimeline
|
|
||||||
const updateRT = () => {
|
|
||||||
if (timelineScroll.position === 'BOTTOM') {
|
|
||||||
trySendingReadReceipt();
|
|
||||||
scroll.from = roomTimeline.timeline.size - scroll.limit - 1;
|
|
||||||
if (scroll.from < 0) scroll.from = 0;
|
|
||||||
scroll.isNewEvent = true;
|
|
||||||
}
|
|
||||||
updateState({});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleScrollToLive = () => {
|
|
||||||
trySendingReadReceipt();
|
|
||||||
scroll.from = roomTimeline.timeline.size - scroll.limit - 1;
|
|
||||||
if (scroll.from < 0) scroll.from = 0;
|
|
||||||
scroll.isNewEvent = true;
|
|
||||||
updateState({});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
trySendingReadReceipt();
|
|
||||||
return () => {
|
|
||||||
setIsReachedTimelineEnd(false);
|
|
||||||
scroll.limit = 0;
|
|
||||||
};
|
|
||||||
}, [roomId]);
|
|
||||||
|
|
||||||
// init room setup completed.
|
|
||||||
// listen for future. setup stateUpdate listener.
|
|
||||||
useEffect(() => {
|
|
||||||
roomTimeline.on(cons.events.roomTimeline.EVENT, updateRT);
|
|
||||||
roomTimeline.on(cons.events.roomTimeline.PAGINATED, updatePAG);
|
|
||||||
viewEvent.on('timeline-scroll', handleTimelineScroll);
|
|
||||||
viewEvent.on('scroll-to-live', handleScrollToLive);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
roomTimeline.removeListener(cons.events.roomTimeline.EVENT, updateRT);
|
|
||||||
roomTimeline.removeListener(cons.events.roomTimeline.PAGINATED, updatePAG);
|
|
||||||
viewEvent.removeListener('timeline-scroll', handleTimelineScroll);
|
|
||||||
viewEvent.removeListener('scroll-to-live', handleScrollToLive);
|
|
||||||
};
|
|
||||||
}, [roomTimeline, isReachedTimelineEnd]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
timelineScroll.reachBottom();
|
|
||||||
autoLoadTimeline();
|
|
||||||
trySendingReadReceipt();
|
|
||||||
}, [roomTimeline]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (onStateUpdate === null || scroll.isNewEvent) {
|
|
||||||
scroll.isNewEvent = false;
|
|
||||||
timelineScroll.reachBottom();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (timelineScroll.isScrollable) {
|
|
||||||
timelineScroll.tryRestoringScroll();
|
|
||||||
} else {
|
|
||||||
timelineScroll.reachBottom();
|
|
||||||
autoLoadTimeline();
|
|
||||||
}
|
|
||||||
}, [onStateUpdate]);
|
|
||||||
|
|
||||||
const handleOnClickCapture = (e) => {
|
|
||||||
const { target } = e;
|
const { target } = e;
|
||||||
const userId = target.getAttribute('data-mx-pill');
|
const userId = target.getAttribute('data-mx-pill');
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
|
const roomId = navigation.selectedRoomId;
|
||||||
openProfileViewer(userId, roomId);
|
openProfileViewer(userId, roomId);
|
||||||
};
|
}
|
||||||
|
|
||||||
let prevMEvent = null;
|
function renderEvent(roomTimeline, mEvent, prevMEvent, isFocus = false) {
|
||||||
const renderMessage = (mEvent) => {
|
const isBodyOnly = (prevMEvent !== null && prevMEvent.getType() !== 'm.room.member'
|
||||||
const isContentOnly = (prevMEvent !== null && prevMEvent.getType() !== 'm.room.member'
|
|
||||||
&& diffMinutes(mEvent.getDate(), prevMEvent.getDate()) <= MAX_MSG_DIFF_MINUTES
|
&& diffMinutes(mEvent.getDate(), prevMEvent.getDate()) <= MAX_MSG_DIFF_MINUTES
|
||||||
&& prevMEvent.getSender() === mEvent.getSender()
|
&& prevMEvent.getSender() === mEvent.getSender()
|
||||||
);
|
);
|
||||||
|
|
||||||
let DividerComp = null;
|
|
||||||
if (prevMEvent !== null && isNotInSameDay(mEvent.getDate(), prevMEvent.getDate())) {
|
|
||||||
DividerComp = <Divider key={`divider-${mEvent.getId()}`} text={`${dateFormat(mEvent.getDate(), 'mmmm dd, yyyy')}`} />;
|
|
||||||
}
|
|
||||||
prevMEvent = mEvent;
|
|
||||||
|
|
||||||
if (mEvent.getType() === 'm.room.member') {
|
if (mEvent.getType() === 'm.room.member') {
|
||||||
const timelineChange = parseTimelineChange(mEvent);
|
const timelineChange = parseTimelineChange(mEvent);
|
||||||
if (timelineChange === null) return false;
|
if (timelineChange === null) return false;
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={`box-${mEvent.getId()}`}>
|
|
||||||
{DividerComp}
|
|
||||||
<TimelineChange
|
<TimelineChange
|
||||||
key={mEvent.getId()}
|
key={mEvent.getId()}
|
||||||
variant={timelineChange.variant}
|
variant={timelineChange.variant}
|
||||||
content={timelineChange.content}
|
content={timelineChange.content}
|
||||||
time={`${dateFormat(mEvent.getDate(), 'hh:MM TT')}`}
|
time={`${dateFormat(mEvent.getDate(), 'hh:MM TT')}`}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={`box-${mEvent.getId()}`}>
|
<Message
|
||||||
{DividerComp}
|
key={mEvent.getId()}
|
||||||
<Message mEvent={mEvent} isBodyOnly={isContentOnly} roomTimeline={roomTimeline} />
|
mEvent={mEvent}
|
||||||
</React.Fragment>
|
isBodyOnly={isBodyOnly}
|
||||||
|
roomTimeline={roomTimeline}
|
||||||
|
focus={isFocus}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimelineScroll extends EventEmitter {
|
||||||
|
constructor(target) {
|
||||||
|
super();
|
||||||
|
if (target === null) {
|
||||||
|
throw new Error('Can not initialize TimelineScroll, target HTMLElement in null');
|
||||||
|
}
|
||||||
|
this.scroll = target;
|
||||||
|
|
||||||
|
this.backwards = false;
|
||||||
|
this.inTopHalf = false;
|
||||||
|
this.maxEvents = 50;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// restore scroll using previous calc by this._updateTopBottomMsg() and this._calcDiff.
|
||||||
|
tryRestoringScroll() {
|
||||||
|
const scrollInfo = getScrollInfo(this.scroll);
|
||||||
|
|
||||||
|
let scrollTop = 0;
|
||||||
|
const ot = this.inTopHalf ? this.topMsg?.offsetTop : this.bottomMsg?.offsetTop;
|
||||||
|
if (!ot) scrollTop = this.top;
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
_calcMaxEvents(scrollInfo) {
|
||||||
|
return Math.round(scrollInfo.viewHeight / SMALLEST_MSG_HEIGHT) * PAGES_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
_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;
|
||||||
|
|
||||||
|
// only calculate maxEvents if viewHeight change
|
||||||
|
if (this.viewHeight !== scrollInfo.viewHeight) {
|
||||||
|
this.maxEvents = this._calcMaxEvents(scrollInfo);
|
||||||
|
this.viewHeight = scrollInfo.viewHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._updateTopBottomMsg();
|
||||||
|
this.diff = this._calcDiff(scrollInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
calcScroll() {
|
||||||
|
if (this.scrolledByCode) {
|
||||||
|
this.scrolledByCode = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollInfo = getScrollInfo(this.scroll);
|
||||||
|
this._updateCalc(scrollInfo);
|
||||||
|
|
||||||
|
this.emit('scroll', this.backwards);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let timelineScroll = null;
|
||||||
|
let focusEventIndex = null;
|
||||||
|
const throttle = new Throttle();
|
||||||
|
|
||||||
|
function useTimeline(roomTimeline, eventId) {
|
||||||
|
const [timelineInfo, setTimelineInfo] = useState(null);
|
||||||
|
|
||||||
|
const initTimeline = (eId) => {
|
||||||
|
setTimelineInfo({
|
||||||
|
focusEventId: eId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
roomTimeline.on(cons.events.roomTimeline.READY, initTimeline);
|
||||||
|
setEventTimeline(eventId);
|
||||||
|
return () => {
|
||||||
|
roomTimeline.removeListener(cons.events.roomTimeline.READY, initTimeline);
|
||||||
|
roomTimeline.removeInternalListeners();
|
||||||
|
};
|
||||||
|
}, [roomTimeline, eventId]);
|
||||||
|
|
||||||
|
return timelineInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useOnPaginate(roomTimeline) {
|
||||||
|
const [info, setInfo] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleOnPagination = (backwards, loaded, canLoadMore) => {
|
||||||
|
setInfo({
|
||||||
|
backwards,
|
||||||
|
loaded,
|
||||||
|
canLoadMore,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handleOnPagination);
|
||||||
|
return () => {
|
||||||
|
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handleOnPagination);
|
||||||
|
};
|
||||||
|
}, [roomTimeline]);
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useAutoPaginate(roomTimeline) {
|
||||||
|
return useCallback(() => {
|
||||||
|
if (roomTimeline.isOngoingPagination) return;
|
||||||
|
|
||||||
|
if (timelineScroll.bottom < SCROLL_TRIGGER_POS && roomTimeline.canPaginateForward()) {
|
||||||
|
roomTimeline.paginateTimeline(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (timelineScroll.top < SCROLL_TRIGGER_POS && roomTimeline.canPaginateBackward()) {
|
||||||
|
roomTimeline.paginateTimeline(true);
|
||||||
|
}
|
||||||
|
}, [roomTimeline]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useHandleScroll(roomTimeline, autoPaginate, viewEvent) {
|
||||||
|
return useCallback(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
// emit event to toggle scrollToBottom button visibility
|
||||||
|
const isAtBottom = timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward();
|
||||||
|
viewEvent.emit('at-bottom', isAtBottom);
|
||||||
|
});
|
||||||
|
autoPaginate();
|
||||||
|
}, [roomTimeline]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useEventArrive(roomTimeline) {
|
||||||
|
const [newEvent, setEvent] = useState(null);
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEvent = (event) => {
|
||||||
|
setEvent(event);
|
||||||
|
};
|
||||||
|
roomTimeline.on(cons.events.roomTimeline.EVENT, handleEvent);
|
||||||
|
return () => {
|
||||||
|
roomTimeline.removeListener(cons.events.roomTimeline.EVENT, handleEvent);
|
||||||
|
};
|
||||||
|
}, [roomTimeline]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!roomTimeline.initialized) return;
|
||||||
|
if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()) {
|
||||||
|
timelineScroll.scrollToBottom();
|
||||||
|
}
|
||||||
|
}, [newEvent, roomTimeline]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RoomViewContent({
|
||||||
|
eventId, roomTimeline, viewEvent,
|
||||||
|
}) {
|
||||||
|
const timelineSVRef = useRef(null);
|
||||||
|
const timelineInfo = useTimeline(roomTimeline, eventId);
|
||||||
|
const readEventStore = useStore(roomTimeline);
|
||||||
|
const paginateInfo = useOnPaginate(roomTimeline);
|
||||||
|
const autoPaginate = useAutoPaginate(roomTimeline);
|
||||||
|
const handleScroll = useHandleScroll(roomTimeline, autoPaginate, viewEvent);
|
||||||
|
useEventArrive(roomTimeline);
|
||||||
|
const { timeline } = roomTimeline;
|
||||||
|
|
||||||
|
const handleScrollToLive = useCallback(() => {
|
||||||
|
if (roomTimeline.isServingLiveTimeline()) {
|
||||||
|
timelineScroll.scrollToBottom();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
roomTimeline.loadLiveTimeline();
|
||||||
|
}, [roomTimeline]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!roomTimeline.initialized) {
|
||||||
|
timelineScroll = new TimelineScroll(timelineSVRef.current);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeline.length > 0) {
|
||||||
|
if (focusEventIndex === null) timelineScroll.scrollToBottom();
|
||||||
|
else timelineScroll.scrollToIndex(focusEventIndex, 80);
|
||||||
|
focusEventIndex = null;
|
||||||
|
}
|
||||||
|
autoPaginate();
|
||||||
|
|
||||||
|
timelineScroll.on('scroll', handleScroll);
|
||||||
|
viewEvent.on('scroll-to-live', handleScrollToLive);
|
||||||
|
return () => {
|
||||||
|
if (timelineSVRef.current === null) return;
|
||||||
|
timelineScroll.removeListener('scroll', handleScroll);
|
||||||
|
viewEvent.removeListener('scroll-to-live', handleScrollToLive);
|
||||||
|
};
|
||||||
|
}, [timelineInfo]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!roomTimeline.initialized) return;
|
||||||
|
// TODO: decide is restore scroll
|
||||||
|
timelineScroll.tryRestoringScroll();
|
||||||
|
autoPaginate();
|
||||||
|
}, [paginateInfo]);
|
||||||
|
|
||||||
|
const handleTimelineScroll = (event) => {
|
||||||
|
const { target } = event;
|
||||||
|
if (!target) return;
|
||||||
|
throttle._(() => timelineScroll?.calcScroll(), 200)(target);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getReadEvent = () => {
|
||||||
|
const readEventId = roomTimeline.getReadUpToEventId();
|
||||||
|
if (readEventStore.getItem()?.getId() === readEventId) {
|
||||||
|
return readEventStore.getItem();
|
||||||
|
}
|
||||||
|
if (roomTimeline.hasEventInActiveTimeline(readEventId)) {
|
||||||
|
return readEventStore.setItem(
|
||||||
|
roomTimeline.findEventByIdInTimelineSet(readEventId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return readEventStore.setItem(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTimeline = () => {
|
const renderTimeline = () => {
|
||||||
const { timeline } = roomTimeline;
|
|
||||||
const tl = [];
|
const tl = [];
|
||||||
if (timeline.size === 0) return tl;
|
|
||||||
|
|
||||||
let i = 0;
|
const readEvent = getReadEvent();
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
let extraItemCount = 0;
|
||||||
for (const [, mEvent] of timeline.entries()) {
|
focusEventIndex = null;
|
||||||
if (i >= scroll.from) {
|
|
||||||
if (i === scroll.from) {
|
if (roomTimeline.canPaginateBackward()) {
|
||||||
if (mEvent.getType() !== 'm.room.create' && !isReachedTimelineEnd) tl.push(genPlaceholders(1));
|
tl.push(loadingMsgPlaceholders(1, PLACEHOLDER_COUNT));
|
||||||
if (mEvent.getType() !== 'm.room.create' && isReachedTimelineEnd) tl.push(genRoomIntro(undefined, roomTimeline));
|
extraItemCount += PLACEHOLDER_COUNT;
|
||||||
}
|
}
|
||||||
if (mEvent.getType() === 'm.room.create') tl.push(genRoomIntro(mEvent, roomTimeline));
|
for (let i = 0; i < timeline.length; i += 1) {
|
||||||
else tl.push(renderMessage(mEvent));
|
const mEvent = timeline[i];
|
||||||
|
const prevMEvent = timeline[i - 1] ?? null;
|
||||||
|
|
||||||
|
if (i === 0 && !roomTimeline.canPaginateBackward()) {
|
||||||
|
if (mEvent.getType() === 'm.room.create') {
|
||||||
|
tl.push(genRoomIntro(mEvent, roomTimeline));
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
tl.push(genRoomIntro(undefined, roomTimeline));
|
||||||
|
extraItemCount += 1;
|
||||||
}
|
}
|
||||||
i += 1;
|
|
||||||
if (i > scroll.getEndIndex()) break;
|
|
||||||
}
|
}
|
||||||
if (i < timeline.size) tl.push(genPlaceholders(2));
|
const unreadDivider = (readEvent
|
||||||
|
&& prevMEvent?.getTs() <= readEvent.getTs()
|
||||||
|
&& readEvent.getTs() < mEvent.getTs());
|
||||||
|
if (unreadDivider) {
|
||||||
|
tl.push(<Divider key={`new-${readEvent.getId()}`} variant="positive" text="Unread messages" />);
|
||||||
|
if (focusEventIndex === null) focusEventIndex = i + extraItemCount;
|
||||||
|
}
|
||||||
|
const dayDivider = prevMEvent && isNotInSameDay(mEvent.getDate(), prevMEvent.getDate());
|
||||||
|
if (dayDivider) {
|
||||||
|
tl.push(<Divider key={`divider-${mEvent.getId()}`} text={`${dateFormat(mEvent.getDate(), 'mmmm dd, yyyy')}`} />);
|
||||||
|
extraItemCount += 1;
|
||||||
|
}
|
||||||
|
const focusId = timelineInfo.focusEventId;
|
||||||
|
const isFocus = focusId === mEvent.getId() && focusId !== readEvent?.getId();
|
||||||
|
if (isFocus) focusEventIndex = i + extraItemCount;
|
||||||
|
|
||||||
|
tl.push(renderEvent(roomTimeline, mEvent, prevMEvent, isFocus));
|
||||||
|
}
|
||||||
|
if (roomTimeline.canPaginateForward()) {
|
||||||
|
tl.push(loadingMsgPlaceholders(2, PLACEHOLDER_COUNT));
|
||||||
|
}
|
||||||
|
|
||||||
return tl;
|
return tl;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ScrollView onScroll={handleTimelineScroll} ref={timelineSVRef} autoHide>
|
||||||
<div className="room-view__content" onClick={handleOnClickCapture}>
|
<div className="room-view__content" onClick={handleOnClickCapture}>
|
||||||
<div className="timeline__wrapper">
|
<div className="timeline__wrapper">
|
||||||
{ renderTimeline() }
|
{ roomTimeline.initialized ? renderTimeline() : loadingMsgPlaceholders('loading', 3) }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RoomViewContent.defaultProps = {
|
||||||
|
eventId: null,
|
||||||
|
};
|
||||||
RoomViewContent.propTypes = {
|
RoomViewContent.propTypes = {
|
||||||
roomId: PropTypes.string.isRequired,
|
eventId: PropTypes.string,
|
||||||
roomTimeline: PropTypes.shape({}).isRequired,
|
roomTimeline: PropTypes.shape({}).isRequired,
|
||||||
timelineScroll: PropTypes.shape({}).isRequired,
|
|
||||||
viewEvent: PropTypes.shape({}).isRequired,
|
viewEvent: PropTypes.shape({}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -9,5 +9,30 @@
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding-bottom: var(--typing-noti-height);
|
padding-bottom: var(--typing-noti-height);
|
||||||
|
|
||||||
|
& .message,
|
||||||
|
& .ph-msg,
|
||||||
|
& .timeline-change {
|
||||||
|
border-radius: 0 var(--bo-radius) var(--bo-radius) 0;
|
||||||
|
[dir=rtl] & {
|
||||||
|
border-radius: var(--bo-radius) 0 0 var(--bo-radius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .divider {
|
||||||
|
margin: var(--sp-extra-tight) var(--sp-normal);
|
||||||
|
margin-right: var(--sp-extra-tight);
|
||||||
|
padding-left: calc(var(--av-small) + var(--sp-tight));
|
||||||
|
[dir=rtl] & {
|
||||||
|
padding: {
|
||||||
|
left: 0;
|
||||||
|
right: calc(var(--av-small) + var(--sp-tight));
|
||||||
|
}
|
||||||
|
margin: {
|
||||||
|
left: var(--sp-extra-tight);
|
||||||
|
right: var(--sp-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -7,65 +7,115 @@ import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
|
import Button from '../../atoms/button/Button';
|
||||||
import IconButton from '../../atoms/button/IconButton';
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
|
|
||||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||||
|
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||||
|
|
||||||
import { getUsersActionJsx } from './common';
|
import { getUsersActionJsx } from './common';
|
||||||
|
|
||||||
function RoomViewFloating({
|
function useJumpToEvent(roomTimeline) {
|
||||||
roomId, roomTimeline, viewEvent,
|
const [eventId, setEventId] = useState(null);
|
||||||
}) {
|
|
||||||
const [reachedBottom, setReachedBottom] = useState(true);
|
const jumpToEvent = () => {
|
||||||
|
roomTimeline.loadEventTimeline(eventId);
|
||||||
|
setEventId(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelJumpToEvent = () => {
|
||||||
|
setEventId(null);
|
||||||
|
roomTimeline.markAsRead();
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: if user reaches the unread messages with other ways
|
||||||
|
// like by paginating, or loading timeline for that event by other ways ex: clicking on reply.
|
||||||
|
// then setEventId(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const readEventId = roomTimeline.getReadUpToEventId();
|
||||||
|
// we only show "Jump to unread" btn only if the event is not in live timeline.
|
||||||
|
// if event is in live timeline
|
||||||
|
// we will automatically open the timeline from that event
|
||||||
|
if (!roomTimeline.hasEventInLiveTimeline(readEventId)) {
|
||||||
|
setEventId(readEventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setEventId(null);
|
||||||
|
};
|
||||||
|
}, [roomTimeline]);
|
||||||
|
|
||||||
|
return [!!eventId, jumpToEvent, cancelJumpToEvent];
|
||||||
|
}
|
||||||
|
|
||||||
|
function useTypingMembers(roomTimeline) {
|
||||||
const [typingMembers, setTypingMembers] = useState(new Set());
|
const [typingMembers, setTypingMembers] = useState(new Set());
|
||||||
|
|
||||||
|
const updateTyping = (members) => {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
|
members.delete(mx.getUserId());
|
||||||
function isSomeoneTyping(members) {
|
|
||||||
const m = members;
|
|
||||||
m.delete(mx.getUserId());
|
|
||||||
if (m.size === 0) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTypingMessage(members) {
|
|
||||||
const userIds = members;
|
|
||||||
userIds.delete(mx.getUserId());
|
|
||||||
return getUsersActionJsx(roomId, [...userIds], 'typing...');
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTyping(members) {
|
|
||||||
setTypingMembers(members);
|
setTypingMembers(members);
|
||||||
}
|
|
||||||
const handleTimelineScroll = (position) => {
|
|
||||||
setReachedBottom(position === 'BOTTOM');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setReachedBottom(true);
|
|
||||||
setTypingMembers(new Set());
|
setTypingMembers(new Set());
|
||||||
viewEvent.on('timeline-scroll', handleTimelineScroll);
|
|
||||||
return () => viewEvent.removeListener('timeline-scroll', handleTimelineScroll);
|
|
||||||
}, [roomId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
roomTimeline.on(cons.events.roomTimeline.TYPING_MEMBERS_UPDATED, updateTyping);
|
roomTimeline.on(cons.events.roomTimeline.TYPING_MEMBERS_UPDATED, updateTyping);
|
||||||
return () => {
|
return () => {
|
||||||
roomTimeline?.removeListener(cons.events.roomTimeline.TYPING_MEMBERS_UPDATED, updateTyping);
|
roomTimeline?.removeListener(cons.events.roomTimeline.TYPING_MEMBERS_UPDATED, updateTyping);
|
||||||
};
|
};
|
||||||
}, [roomTimeline]);
|
}, [roomTimeline]);
|
||||||
|
|
||||||
|
return [typingMembers];
|
||||||
|
}
|
||||||
|
|
||||||
|
function useScrollToBottom(roomId, viewEvent) {
|
||||||
|
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||||
|
const handleAtBottom = (atBottom) => setIsAtBottom(atBottom);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsAtBottom(true);
|
||||||
|
viewEvent.on('at-bottom', handleAtBottom);
|
||||||
|
return () => viewEvent.removeListener('at-bottom', handleAtBottom);
|
||||||
|
}, [roomId]);
|
||||||
|
|
||||||
|
return [isAtBottom, setIsAtBottom];
|
||||||
|
}
|
||||||
|
|
||||||
|
function RoomViewFloating({
|
||||||
|
roomId, roomTimeline, viewEvent,
|
||||||
|
}) {
|
||||||
|
const [isJumpToEvent, jumpToEvent, cancelJumpToEvent] = useJumpToEvent(roomTimeline, viewEvent);
|
||||||
|
const [typingMembers] = useTypingMembers(roomTimeline);
|
||||||
|
const [isAtBottom, setIsAtBottom] = useScrollToBottom(roomId, viewEvent);
|
||||||
|
|
||||||
|
const handleScrollToBottom = () => {
|
||||||
|
viewEvent.emit('scroll-to-live');
|
||||||
|
setIsAtBottom(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`room-view__typing${isSomeoneTyping(typingMembers) ? ' room-view__typing--open' : ''}`}>
|
<div className={`room-view__unread ${isJumpToEvent ? 'room-view__unread--open' : ''}`}>
|
||||||
<div className="bouncing-loader"><div /></div>
|
<Button onClick={jumpToEvent} variant="primary">
|
||||||
<Text variant="b2">{getTypingMessage(typingMembers)}</Text>
|
<Text variant="b2">Jump to unread</Text>
|
||||||
</div>
|
</Button>
|
||||||
<div className={`room-view__STB${reachedBottom ? '' : ' room-view__STB--open'}`}>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={cancelJumpToEvent}
|
||||||
viewEvent.emit('scroll-to-live');
|
variant="primary"
|
||||||
setReachedBottom(true);
|
size="extra-small"
|
||||||
}}
|
src={CrossIC}
|
||||||
|
tooltipPlacement="bottom"
|
||||||
|
tooltip="Cancel"
|
||||||
|
/>
|
||||||
|
</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'}`}>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleScrollToBottom}
|
||||||
src={ChevronBottomIC}
|
src={ChevronBottomIC}
|
||||||
tooltip="Scroll to Bottom"
|
tooltip="Scroll to Bottom"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -88,4 +88,35 @@
|
||||||
transform: translateY(-28px) scale(1);
|
transform: translateY(-28px) scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__unread {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--sp-extra-tight);
|
||||||
|
right: var(--sp-extra-tight);
|
||||||
|
z-index: 999;
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
background-color: var(--bg-surface);
|
||||||
|
border-radius: var(--bo-radius);
|
||||||
|
box-shadow: var(--bs-primary-border);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&--open {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .ic-btn {
|
||||||
|
padding: 6px var(--sp-extra-tight);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
& .btn-primary {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0 var(--sp-tight);
|
||||||
|
&:focus {
|
||||||
|
background-color: var(--bg-primary-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -35,7 +35,7 @@ let isTyping = false;
|
||||||
let isCmdActivated = false;
|
let isCmdActivated = false;
|
||||||
let cmdCursorPos = null;
|
let cmdCursorPos = null;
|
||||||
function RoomViewInput({
|
function RoomViewInput({
|
||||||
roomId, roomTimeline, timelineScroll, viewEvent,
|
roomId, roomTimeline, viewEvent,
|
||||||
}) {
|
}) {
|
||||||
const [attachment, setAttachment] = useState(null);
|
const [attachment, setAttachment] = useState(null);
|
||||||
const [isMarkdown, setIsMarkdown] = useState(settings.isMarkdown);
|
const [isMarkdown, setIsMarkdown] = useState(settings.isMarkdown);
|
||||||
|
@ -211,7 +211,6 @@ function RoomViewInput({
|
||||||
focusInput();
|
focusInput();
|
||||||
|
|
||||||
textAreaRef.current.value = roomsInput.getMessage(roomId);
|
textAreaRef.current.value = roomsInput.getMessage(roomId);
|
||||||
viewEvent.emit('scroll-to-live');
|
|
||||||
viewEvent.emit('message_sent');
|
viewEvent.emit('message_sent');
|
||||||
textAreaRef.current.style.height = 'unset';
|
textAreaRef.current.style.height = 'unset';
|
||||||
if (replyTo !== null) setReplyTo(null);
|
if (replyTo !== null) setReplyTo(null);
|
||||||
|
@ -344,14 +343,13 @@ function RoomViewInput({
|
||||||
<IconButton onClick={handleUploadClick} tooltip={attachment === null ? 'Upload' : 'Cancel'} src={CirclePlusIC} />
|
<IconButton onClick={handleUploadClick} tooltip={attachment === null ? 'Upload' : 'Cancel'} src={CirclePlusIC} />
|
||||||
</div>
|
</div>
|
||||||
<div ref={inputBaseRef} className="room-input__input-container">
|
<div ref={inputBaseRef} className="room-input__input-container">
|
||||||
{roomTimeline.isEncryptedRoom() && <RawIcon size="extra-small" src={ShieldIC} />}
|
{roomTimeline.isEncrypted() && <RawIcon size="extra-small" src={ShieldIC} />}
|
||||||
<ScrollView autoHide>
|
<ScrollView autoHide>
|
||||||
<Text className="room-input__textarea-wrapper">
|
<Text className="room-input__textarea-wrapper">
|
||||||
<TextareaAutosize
|
<TextareaAutosize
|
||||||
ref={textAreaRef}
|
ref={textAreaRef}
|
||||||
onChange={handleMsgTyping}
|
onChange={handleMsgTyping}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
onResize={() => timelineScroll.autoReachBottom()}
|
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="Send a message..."
|
placeholder="Send a message..."
|
||||||
/>
|
/>
|
||||||
|
@ -434,7 +432,6 @@ function RoomViewInput({
|
||||||
RoomViewInput.propTypes = {
|
RoomViewInput.propTypes = {
|
||||||
roomId: PropTypes.string.isRequired,
|
roomId: PropTypes.string.isRequired,
|
||||||
roomTimeline: PropTypes.shape({}).isRequired,
|
roomTimeline: PropTypes.shape({}).isRequired,
|
||||||
timelineScroll: PropTypes.shape({}).isRequired,
|
|
||||||
viewEvent: PropTypes.shape({}).isRequired,
|
viewEvent: PropTypes.shape({}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -15,10 +15,11 @@ function selectSpace(roomId) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectRoom(roomId) {
|
function selectRoom(roomId, eventId) {
|
||||||
appDispatcher.dispatch({
|
appDispatcher.dispatch({
|
||||||
type: cons.actions.navigation.SELECT_ROOM,
|
type: cons.actions.navigation.SELECT_ROOM,
|
||||||
roomId,
|
roomId,
|
||||||
|
eventId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,11 +72,11 @@ function openEmojiBoard(cords, requestEmojiCallback) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function openReadReceipts(roomId, eventId) {
|
function openReadReceipts(roomId, userIds) {
|
||||||
appDispatcher.dispatch({
|
appDispatcher.dispatch({
|
||||||
type: cons.actions.navigation.OPEN_READRECEIPTS,
|
type: cons.actions.navigation.OPEN_READRECEIPTS,
|
||||||
roomId,
|
roomId,
|
||||||
eventId,
|
userIds,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import * as sdk from 'matrix-js-sdk';
|
import * as sdk from 'matrix-js-sdk';
|
||||||
import { logger } from 'matrix-js-sdk/lib/logger';
|
// import { logger } from 'matrix-js-sdk/lib/logger';
|
||||||
|
|
||||||
import { secret } from './state/auth';
|
import { secret } from './state/auth';
|
||||||
import RoomList from './state/RoomList';
|
import RoomList from './state/RoomList';
|
||||||
|
@ -9,7 +9,7 @@ import Notifications from './state/Notifications';
|
||||||
|
|
||||||
global.Olm = require('@matrix-org/olm');
|
global.Olm = require('@matrix-org/olm');
|
||||||
|
|
||||||
logger.disableAll();
|
// logger.disableAll();
|
||||||
|
|
||||||
class InitMatrix extends EventEmitter {
|
class InitMatrix extends EventEmitter {
|
||||||
async init() {
|
async init() {
|
||||||
|
|
|
@ -34,16 +34,17 @@ class Notifications extends EventEmitter {
|
||||||
doesRoomHaveUnread(room) {
|
doesRoomHaveUnread(room) {
|
||||||
const userId = this.matrixClient.getUserId();
|
const userId = this.matrixClient.getUserId();
|
||||||
const readUpToId = room.getEventReadUpTo(userId);
|
const readUpToId = room.getEventReadUpTo(userId);
|
||||||
|
const liveEvents = room.getLiveTimeline().getEvents();
|
||||||
|
|
||||||
if (room.timeline.length
|
if (liveEvents.length
|
||||||
&& room.timeline[room.timeline.length - 1].sender
|
&& liveEvents[liveEvents.length - 1].sender
|
||||||
&& room.timeline[room.timeline.length - 1].sender.userId === userId
|
&& liveEvents[liveEvents.length - 1].sender.userId === userId
|
||||||
&& room.timeline[room.timeline.length - 1].getType() !== 'm.room.member') {
|
&& liveEvents[liveEvents.length - 1].getType() !== 'm.room.member') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = room.timeline.length - 1; i >= 0; i -= 1) {
|
for (let i = liveEvents.length - 1; i >= 0; i -= 1) {
|
||||||
const event = room.timeline[i];
|
const event = liveEvents[i];
|
||||||
|
|
||||||
if (event.getId() === readUpToId) return false;
|
if (event.getId() === readUpToId) return false;
|
||||||
|
|
||||||
|
@ -150,8 +151,9 @@ class Notifications extends EventEmitter {
|
||||||
_listenEvents() {
|
_listenEvents() {
|
||||||
this.matrixClient.on('Room.timeline', (mEvent, room) => {
|
this.matrixClient.on('Room.timeline', (mEvent, room) => {
|
||||||
if (!this.supportEvents.includes(mEvent.getType())) return;
|
if (!this.supportEvents.includes(mEvent.getType())) return;
|
||||||
|
const liveEvents = room.getLiveTimeline().getEvents();
|
||||||
|
|
||||||
const lastTimelineEvent = room.timeline[room.timeline.length - 1];
|
const lastTimelineEvent = liveEvents[liveEvents.length - 1];
|
||||||
if (lastTimelineEvent.getId() !== mEvent.getId()) return;
|
if (lastTimelineEvent.getId() !== mEvent.getId()) return;
|
||||||
if (mEvent.getSender() === this.matrixClient.getUserId()) return;
|
if (mEvent.getSender() === this.matrixClient.getUserId()) return;
|
||||||
|
|
||||||
|
|
|
@ -24,52 +24,289 @@ function addToMap(myMap, mEvent) {
|
||||||
return mEvent;
|
return mEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getFirstLinkedTimeline(timeline) {
|
||||||
|
let tm = timeline;
|
||||||
|
while (tm.prevTimeline) {
|
||||||
|
tm = tm.prevTimeline;
|
||||||
|
}
|
||||||
|
return tm;
|
||||||
|
}
|
||||||
|
function getLastLinkedTimeline(timeline) {
|
||||||
|
let tm = timeline;
|
||||||
|
while (tm.nextTimeline) {
|
||||||
|
tm = tm.nextTimeline;
|
||||||
|
}
|
||||||
|
return tm;
|
||||||
|
}
|
||||||
|
|
||||||
|
function iterateLinkedTimelines(timeline, backwards, callback) {
|
||||||
|
let tm = timeline;
|
||||||
|
while (tm) {
|
||||||
|
callback(tm);
|
||||||
|
if (backwards) tm = tm.prevTimeline;
|
||||||
|
else tm = tm.nextTimeline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class RoomTimeline extends EventEmitter {
|
class RoomTimeline extends EventEmitter {
|
||||||
constructor(roomId) {
|
constructor(roomId) {
|
||||||
super();
|
super();
|
||||||
|
// These are local timelines
|
||||||
|
this.timeline = [];
|
||||||
|
this.editedTimeline = new Map();
|
||||||
|
this.reactionTimeline = new Map();
|
||||||
|
this.typingMembers = new Set();
|
||||||
|
|
||||||
this.matrixClient = initMatrix.matrixClient;
|
this.matrixClient = initMatrix.matrixClient;
|
||||||
this.roomId = roomId;
|
this.roomId = roomId;
|
||||||
this.room = this.matrixClient.getRoom(roomId);
|
this.room = this.matrixClient.getRoom(roomId);
|
||||||
|
|
||||||
this.timeline = new Map();
|
this.liveTimeline = this.room.getLiveTimeline();
|
||||||
this.editedTimeline = new Map();
|
this.activeTimeline = this.liveTimeline;
|
||||||
this.reactionTimeline = new Map();
|
|
||||||
|
|
||||||
this.isOngoingPagination = false;
|
this.isOngoingPagination = false;
|
||||||
this.ongoingDecryptionCount = 0;
|
this.ongoingDecryptionCount = 0;
|
||||||
this.typingMembers = new Set();
|
this.initialized = false;
|
||||||
|
|
||||||
this._listenRoomTimeline = (event, room) => {
|
// TODO: remove below line
|
||||||
|
window.selectedRoom = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
isServingLiveTimeline() {
|
||||||
|
return getLastLinkedTimeline(this.activeTimeline) === this.liveTimeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
canPaginateBackward() {
|
||||||
|
const tm = getFirstLinkedTimeline(this.activeTimeline);
|
||||||
|
return tm.getPaginationToken('b') !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
canPaginateForward() {
|
||||||
|
return !this.isServingLiveTimeline();
|
||||||
|
}
|
||||||
|
|
||||||
|
isEncrypted() {
|
||||||
|
return this.matrixClient.isRoomEncrypted(this.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearLocalTimelines() {
|
||||||
|
this.timeline = [];
|
||||||
|
this.reactionTimeline.clear();
|
||||||
|
this.editedTimeline.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
addToTimeline(mEvent) {
|
||||||
|
if (mEvent.isRedacted()) return;
|
||||||
|
if (isReaction(mEvent)) {
|
||||||
|
addToMap(this.reactionTimeline, mEvent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!cons.supportEventTypes.includes(mEvent.getType())) return;
|
||||||
|
if (isEdited(mEvent)) {
|
||||||
|
addToMap(this.editedTimeline, mEvent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.timeline.push(mEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
_populateAllLinkedEvents(timeline) {
|
||||||
|
const firstTimeline = getFirstLinkedTimeline(timeline);
|
||||||
|
iterateLinkedTimelines(firstTimeline, false, (tm) => {
|
||||||
|
tm.getEvents().forEach((mEvent) => this.addToTimeline(mEvent));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_populateTimelines() {
|
||||||
|
this.clearLocalTimelines();
|
||||||
|
this._populateAllLinkedEvents(this.activeTimeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _reset(eventId) {
|
||||||
|
if (this.isEncrypted()) await this.decryptAllEventsOfTimeline(this.activeTimeline);
|
||||||
|
this._populateTimelines();
|
||||||
|
if (!this.initialized) {
|
||||||
|
this.initialized = true;
|
||||||
|
this._listenEvents();
|
||||||
|
}
|
||||||
|
this.emit(cons.events.roomTimeline.READY, eventId ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadLiveTimeline() {
|
||||||
|
this.activeTimeline = this.liveTimeline;
|
||||||
|
await this._reset();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadEventTimeline(eventId) {
|
||||||
|
// we use first unfiltered EventTimelineSet for room pagination.
|
||||||
|
const timelineSet = this.getUnfilteredTimelineSet();
|
||||||
|
try {
|
||||||
|
const eventTimeline = await this.matrixClient.getEventTimeline(timelineSet, eventId);
|
||||||
|
this.activeTimeline = eventTimeline;
|
||||||
|
await this._reset(eventId);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async paginateTimeline(backwards = false, limit = 30) {
|
||||||
|
if (this.initialized === false) return false;
|
||||||
|
if (this.isOngoingPagination) return false;
|
||||||
|
|
||||||
|
this.isOngoingPagination = true;
|
||||||
|
|
||||||
|
const timelineToPaginate = backwards
|
||||||
|
? getFirstLinkedTimeline(this.activeTimeline)
|
||||||
|
: getLastLinkedTimeline(this.activeTimeline);
|
||||||
|
|
||||||
|
if (timelineToPaginate.getPaginationToken(backwards ? 'b' : 'f') === null) {
|
||||||
|
this.isOngoingPagination = false;
|
||||||
|
this.emit(cons.events.roomTimeline.PAGINATED, backwards, 0, false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldSize = this.timeline.length;
|
||||||
|
try {
|
||||||
|
const canPaginateMore = await this.matrixClient
|
||||||
|
.paginateEventTimeline(timelineToPaginate, { backwards, limit });
|
||||||
|
|
||||||
|
if (this.isEncrypted()) await this.decryptAllEventsOfTimeline(this.activeTimeline);
|
||||||
|
this._populateTimelines();
|
||||||
|
|
||||||
|
const loaded = this.timeline.length - oldSize;
|
||||||
|
this.isOngoingPagination = false;
|
||||||
|
this.emit(cons.events.roomTimeline.PAGINATED, backwards, loaded, canPaginateMore);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
this.isOngoingPagination = false;
|
||||||
|
this.emit(cons.events.roomTimeline.PAGINATED, backwards, 0, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptAllEventsOfTimeline(eventTimeline) {
|
||||||
|
const decryptionPromises = eventTimeline
|
||||||
|
.getEvents()
|
||||||
|
.filter((event) => event.isEncrypted() && !event.clearEvent)
|
||||||
|
.reverse()
|
||||||
|
.map((event) => event.attemptDecryption(this.matrixClient.crypto, { isRetry: true }));
|
||||||
|
|
||||||
|
return Promise.allSettled(decryptionPromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
markAsRead() {
|
||||||
|
const readEventId = this.getReadUpToEventId();
|
||||||
|
if (this.timeline.length === 0) return;
|
||||||
|
const latestEvent = this.timeline[this.timeline.length - 1];
|
||||||
|
if (readEventId === latestEvent.getId()) return;
|
||||||
|
this.matrixClient.sendReadReceipt(latestEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasEventInLiveTimeline(eventId) {
|
||||||
|
const timelineSet = this.getUnfilteredTimelineSet();
|
||||||
|
return timelineSet.getTimelineForEvent(eventId) === this.liveTimeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasEventInActiveTimeline(eventId) {
|
||||||
|
const timelineSet = this.getUnfilteredTimelineSet();
|
||||||
|
return timelineSet.getTimelineForEvent(eventId) === this.activeTimeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUnfilteredTimelineSet() {
|
||||||
|
return this.room.getUnfilteredTimelineSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
getLiveReaders() {
|
||||||
|
const lastEvent = this.timeline[this.timeline.length - 1];
|
||||||
|
const liveEvents = this.liveTimeline.getEvents();
|
||||||
|
const lastLiveEvent = liveEvents[liveEvents.length - 1];
|
||||||
|
|
||||||
|
let readers = [];
|
||||||
|
if (lastEvent) readers = this.room.getUsersReadUpTo(lastEvent);
|
||||||
|
if (lastLiveEvent !== lastEvent) {
|
||||||
|
readers.splice(readers.length, 0, ...this.room.getUsersReadUpTo(lastLiveEvent));
|
||||||
|
}
|
||||||
|
return [...new Set(readers)];
|
||||||
|
}
|
||||||
|
|
||||||
|
getEventReaders(eventId) {
|
||||||
|
const readers = [];
|
||||||
|
let eventIndex = this.getEventIndex(eventId);
|
||||||
|
if (eventIndex < 0) return this.getLiveReaders();
|
||||||
|
for (; eventIndex < this.timeline.length; eventIndex += 1) {
|
||||||
|
readers.splice(readers.length, 0, ...this.room.getUsersReadUpTo(this.timeline[eventIndex]));
|
||||||
|
}
|
||||||
|
return [...new Set(readers)];
|
||||||
|
}
|
||||||
|
|
||||||
|
getReadUpToEventId() {
|
||||||
|
return this.room.getEventReadUpTo(this.matrixClient.getUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
getEventIndex(eventId) {
|
||||||
|
return this.timeline.findIndex((mEvent) => mEvent.getId() === eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
findEventByIdInTimelineSet(eventId, eventTimelineSet = this.getUnfilteredTimelineSet()) {
|
||||||
|
return eventTimelineSet.findEventById(eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
findEventById(eventId) {
|
||||||
|
return this.timeline[this.getEventIndex(eventId)] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteFromTimeline(eventId) {
|
||||||
|
const i = this.getEventIndex(eventId);
|
||||||
|
if (i === -1) return undefined;
|
||||||
|
return this.timeline.splice(i, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
_listenEvents() {
|
||||||
|
this._listenRoomTimeline = (event, room, toStartOfTimeline, removed, data) => {
|
||||||
if (room.roomId !== this.roomId) return;
|
if (room.roomId !== this.roomId) return;
|
||||||
|
if (this.isOngoingPagination) return;
|
||||||
|
|
||||||
|
// User is currently viewing the old events probably
|
||||||
|
// no need to add this event and emit changes.
|
||||||
|
if (this.isServingLiveTimeline() === false) return;
|
||||||
|
|
||||||
|
// We only process live events here
|
||||||
|
if (!data.liveEvent) return;
|
||||||
|
|
||||||
if (event.isEncrypted()) {
|
if (event.isEncrypted()) {
|
||||||
|
// We will add this event after it is being decrypted.
|
||||||
this.ongoingDecryptionCount += 1;
|
this.ongoingDecryptionCount += 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.ongoingDecryptionCount !== 0) return;
|
// FIXME: An unencrypted plain event can come
|
||||||
if (this.isOngoingPagination) return;
|
// while previous event is still decrypting
|
||||||
|
// and has not been added to timeline
|
||||||
|
// causing unordered timeline view.
|
||||||
|
|
||||||
this.addToTimeline(event);
|
this.addToTimeline(event);
|
||||||
this.emit(cons.events.roomTimeline.EVENT);
|
this.emit(cons.events.roomTimeline.EVENT, event);
|
||||||
};
|
};
|
||||||
|
|
||||||
this._listenDecryptEvent = (event) => {
|
this._listenDecryptEvent = (event) => {
|
||||||
if (event.getRoomId() !== this.roomId) return;
|
if (event.getRoomId() !== this.roomId) return;
|
||||||
|
if (this.isOngoingPagination) return;
|
||||||
|
|
||||||
|
// Not a live event.
|
||||||
|
// so we don't need to process it here
|
||||||
|
if (this.ongoingDecryptionCount === 0) return;
|
||||||
|
|
||||||
if (this.ongoingDecryptionCount > 0) {
|
if (this.ongoingDecryptionCount > 0) {
|
||||||
this.ongoingDecryptionCount -= 1;
|
this.ongoingDecryptionCount -= 1;
|
||||||
}
|
}
|
||||||
if (this.ongoingDecryptionCount > 0) return;
|
|
||||||
|
|
||||||
if (this.isOngoingPagination) return;
|
|
||||||
this.addToTimeline(event);
|
this.addToTimeline(event);
|
||||||
this.emit(cons.events.roomTimeline.EVENT);
|
this.emit(cons.events.roomTimeline.EVENT, event);
|
||||||
};
|
};
|
||||||
|
|
||||||
this._listenRedaction = (event, room) => {
|
this._listenRedaction = (event, room) => {
|
||||||
if (room.roomId !== this.roomId) return;
|
if (room.roomId !== this.roomId) return;
|
||||||
this.timeline.delete(event.getId());
|
this.deleteFromTimeline(event.getId());
|
||||||
this.editedTimeline.delete(event.getId());
|
this.editedTimeline.delete(event.getId());
|
||||||
this.reactionTimeline.delete(event.getId());
|
this.reactionTimeline.delete(event.getId());
|
||||||
this.emit(cons.events.roomTimeline.EVENT);
|
this.emit(cons.events.roomTimeline.EVENT);
|
||||||
|
@ -84,15 +321,18 @@ class RoomTimeline extends EventEmitter {
|
||||||
this.emit(cons.events.roomTimeline.TYPING_MEMBERS_UPDATED, new Set([...this.typingMembers]));
|
this.emit(cons.events.roomTimeline.TYPING_MEMBERS_UPDATED, new Set([...this.typingMembers]));
|
||||||
};
|
};
|
||||||
this._listenReciptEvent = (event, room) => {
|
this._listenReciptEvent = (event, room) => {
|
||||||
|
// we only process receipt for latest message here.
|
||||||
if (room.roomId !== this.roomId) return;
|
if (room.roomId !== this.roomId) return;
|
||||||
const receiptContent = event.getContent();
|
const receiptContent = event.getContent();
|
||||||
if (this.timeline.length === 0) return;
|
|
||||||
const tmlLastEvent = room.timeline[room.timeline.length - 1];
|
const mEvents = this.liveTimeline.getEvents();
|
||||||
const lastEventId = tmlLastEvent.getId();
|
const lastMEvent = mEvents[mEvents.length - 1];
|
||||||
|
const lastEventId = lastMEvent.getId();
|
||||||
const lastEventRecipt = receiptContent[lastEventId];
|
const lastEventRecipt = receiptContent[lastEventId];
|
||||||
|
|
||||||
if (typeof lastEventRecipt === 'undefined') return;
|
if (typeof lastEventRecipt === 'undefined') return;
|
||||||
if (lastEventRecipt['m.read']) {
|
if (lastEventRecipt['m.read']) {
|
||||||
this.emit(cons.events.roomTimeline.READ_RECEIPT);
|
this.emit(cons.events.roomTimeline.LIVE_RECEIPT);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -101,62 +341,10 @@ class RoomTimeline extends EventEmitter {
|
||||||
this.matrixClient.on('Event.decrypted', this._listenDecryptEvent);
|
this.matrixClient.on('Event.decrypted', this._listenDecryptEvent);
|
||||||
this.matrixClient.on('RoomMember.typing', this._listenTypingEvent);
|
this.matrixClient.on('RoomMember.typing', this._listenTypingEvent);
|
||||||
this.matrixClient.on('Room.receipt', this._listenReciptEvent);
|
this.matrixClient.on('Room.receipt', this._listenReciptEvent);
|
||||||
|
|
||||||
// TODO: remove below line when release
|
|
||||||
window.selectedRoom = this;
|
|
||||||
|
|
||||||
if (this.isEncryptedRoom()) this.room.decryptAllEvents();
|
|
||||||
this._populateTimelines();
|
|
||||||
}
|
|
||||||
|
|
||||||
isEncryptedRoom() {
|
|
||||||
return this.matrixClient.isRoomEncrypted(this.roomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
addToTimeline(mEvent) {
|
|
||||||
if (isReaction(mEvent)) {
|
|
||||||
addToMap(this.reactionTimeline, mEvent);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!cons.supportEventTypes.includes(mEvent.getType())) return;
|
|
||||||
if (isEdited(mEvent)) {
|
|
||||||
addToMap(this.editedTimeline, mEvent);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.timeline.set(mEvent.getId(), mEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
_populateTimelines() {
|
|
||||||
this.timeline.clear();
|
|
||||||
this.reactionTimeline.clear();
|
|
||||||
this.editedTimeline.clear();
|
|
||||||
this.room.timeline.forEach((mEvent) => this.addToTimeline(mEvent));
|
|
||||||
}
|
|
||||||
|
|
||||||
paginateBack() {
|
|
||||||
if (this.isOngoingPagination) return;
|
|
||||||
this.isOngoingPagination = true;
|
|
||||||
|
|
||||||
const oldSize = this.timeline.size;
|
|
||||||
const MSG_LIMIT = 30;
|
|
||||||
this.matrixClient.scrollback(this.room, MSG_LIMIT).then(async (room) => {
|
|
||||||
if (room.oldState.paginationToken === null) {
|
|
||||||
// We have reached start of the timeline
|
|
||||||
this.isOngoingPagination = false;
|
|
||||||
if (this.isEncryptedRoom()) await this.room.decryptAllEvents();
|
|
||||||
this.emit(cons.events.roomTimeline.PAGINATED, false, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._populateTimelines();
|
|
||||||
const loaded = this.timeline.size - oldSize;
|
|
||||||
|
|
||||||
if (this.isEncryptedRoom()) await this.room.decryptAllEvents();
|
|
||||||
this.isOngoingPagination = false;
|
|
||||||
this.emit(cons.events.roomTimeline.PAGINATED, true, loaded);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeInternalListeners() {
|
removeInternalListeners() {
|
||||||
|
if (!this.initialized) return;
|
||||||
this.matrixClient.removeListener('Room.timeline', this._listenRoomTimeline);
|
this.matrixClient.removeListener('Room.timeline', this._listenRoomTimeline);
|
||||||
this.matrixClient.removeListener('Room.redaction', this._listenRedaction);
|
this.matrixClient.removeListener('Room.redaction', this._listenRedaction);
|
||||||
this.matrixClient.removeListener('Event.decrypted', this._listenDecryptEvent);
|
this.matrixClient.removeListener('Event.decrypted', this._listenDecryptEvent);
|
||||||
|
|
|
@ -81,10 +81,11 @@ const cons = {
|
||||||
FULL_READ: 'FULL_READ',
|
FULL_READ: 'FULL_READ',
|
||||||
},
|
},
|
||||||
roomTimeline: {
|
roomTimeline: {
|
||||||
|
READY: 'READY',
|
||||||
EVENT: 'EVENT',
|
EVENT: 'EVENT',
|
||||||
PAGINATED: 'PAGINATED',
|
PAGINATED: 'PAGINATED',
|
||||||
TYPING_MEMBERS_UPDATED: 'TYPING_MEMBERS_UPDATED',
|
TYPING_MEMBERS_UPDATED: 'TYPING_MEMBERS_UPDATED',
|
||||||
READ_RECEIPT: 'READ_RECEIPT',
|
LIVE_RECEIPT: 'LIVE_RECEIPT',
|
||||||
},
|
},
|
||||||
roomsInput: {
|
roomsInput: {
|
||||||
MESSAGE_SENT: 'MESSAGE_SENT',
|
MESSAGE_SENT: 'MESSAGE_SENT',
|
||||||
|
|
|
@ -50,7 +50,12 @@ class Navigation extends EventEmitter {
|
||||||
[cons.actions.navigation.SELECT_ROOM]: () => {
|
[cons.actions.navigation.SELECT_ROOM]: () => {
|
||||||
const prevSelectedRoomId = this.selectedRoomId;
|
const prevSelectedRoomId = this.selectedRoomId;
|
||||||
this.selectedRoomId = action.roomId;
|
this.selectedRoomId = action.roomId;
|
||||||
this.emit(cons.events.navigation.ROOM_SELECTED, this.selectedRoomId, prevSelectedRoomId);
|
this.emit(
|
||||||
|
cons.events.navigation.ROOM_SELECTED,
|
||||||
|
this.selectedRoomId,
|
||||||
|
prevSelectedRoomId,
|
||||||
|
action.eventId,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[cons.actions.navigation.OPEN_INVITE_LIST]: () => {
|
[cons.actions.navigation.OPEN_INVITE_LIST]: () => {
|
||||||
this.emit(cons.events.navigation.INVITE_LIST_OPENED);
|
this.emit(cons.events.navigation.INVITE_LIST_OPENED);
|
||||||
|
@ -80,7 +85,7 @@ class Navigation extends EventEmitter {
|
||||||
this.emit(
|
this.emit(
|
||||||
cons.events.navigation.READRECEIPTS_OPENED,
|
cons.events.navigation.READRECEIPTS_OPENED,
|
||||||
action.roomId,
|
action.roomId,
|
||||||
action.eventId,
|
action.userIds,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[cons.actions.navigation.OPEN_ROOMOPTIONS]: () => {
|
[cons.actions.navigation.OPEN_ROOMOPTIONS]: () => {
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
--bg-badge: #989898;
|
--bg-badge: #989898;
|
||||||
--bg-ping: hsla(137deg, 100%, 68%, 40%);
|
--bg-ping: hsla(137deg, 100%, 68%, 40%);
|
||||||
--bg-ping-hover: hsla(137deg, 100%, 68%, 50%);
|
--bg-ping-hover: hsla(137deg, 100%, 68%, 50%);
|
||||||
|
--bg-divider: hsla(0, 0%, 0%, .1);
|
||||||
|
|
||||||
/* text color | --tc-[background type]-[priority]: value */
|
/* text color | --tc-[background type]-[priority]: value */
|
||||||
--tc-surface-high: #000000;
|
--tc-surface-high: #000000;
|
||||||
|
@ -187,6 +188,7 @@
|
||||||
--bg-badge: hsl(0, 0%, 75%);
|
--bg-badge: hsl(0, 0%, 75%);
|
||||||
--bg-ping: hsla(137deg, 100%, 38%, 40%);
|
--bg-ping: hsla(137deg, 100%, 38%, 40%);
|
||||||
--bg-ping-hover: hsla(137deg, 100%, 38%, 50%);
|
--bg-ping-hover: hsla(137deg, 100%, 38%, 50%);
|
||||||
|
--bg-divider: hsla(0, 0%, 100%, .1);
|
||||||
|
|
||||||
|
|
||||||
/* text color | --tc-[background type]-[priority]: value */
|
/* text color | --tc-[background type]-[priority]: value */
|
||||||
|
|
|
@ -90,7 +90,6 @@ export function getScrollInfo(target) {
|
||||||
scroll.top = Math.round(target.scrollTop);
|
scroll.top = Math.round(target.scrollTop);
|
||||||
scroll.height = Math.round(target.scrollHeight);
|
scroll.height = Math.round(target.scrollHeight);
|
||||||
scroll.viewHeight = Math.round(target.offsetHeight);
|
scroll.viewHeight = Math.round(target.offsetHeight);
|
||||||
scroll.bottom = Math.round(scroll.top + scroll.viewHeight);
|
|
||||||
scroll.isScrollable = scroll.height > scroll.viewHeight;
|
scroll.isScrollable = scroll.height > scroll.viewHeight;
|
||||||
return scroll;
|
return scroll;
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,7 +56,28 @@ function getPowerLabel(powerLevel) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseReply(rawBody) {
|
||||||
|
if (rawBody?.indexOf('>') !== 0) return null;
|
||||||
|
let body = rawBody.slice(rawBody.indexOf('<') + 1);
|
||||||
|
const user = body.slice(0, body.indexOf('>'));
|
||||||
|
|
||||||
|
body = body.slice(body.indexOf('>') + 2);
|
||||||
|
const replyBody = body.slice(0, body.indexOf('\n\n'));
|
||||||
|
body = body.slice(body.indexOf('\n\n') + 2);
|
||||||
|
|
||||||
|
if (user === '') return null;
|
||||||
|
|
||||||
|
const isUserId = user.match(/^@.+:.+/);
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: isUserId ? user : null,
|
||||||
|
displayName: isUserId ? null : user,
|
||||||
|
replyBody,
|
||||||
|
body,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getBaseUrl, getUsername, getUsernameOfRoomMember,
|
getBaseUrl, getUsername, getUsernameOfRoomMember,
|
||||||
isRoomAliasAvailable, getPowerLabel,
|
isRoomAliasAvailable, getPowerLabel, parseReply,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue