Add ability to search room messages

Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
Ajay Bura 2022-01-16 14:17:50 +05:30
parent eddba3c652
commit dcef08009d
6 changed files with 289 additions and 22 deletions

View file

@ -52,17 +52,14 @@ function PlaceholderMessage() {
}
const MessageAvatar = React.memo(({
roomId, mEvent, userId, username,
}) => {
const avatarSrc = mEvent.sender.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop');
return (
<div className="message__avatar-container">
<button type="button" onClick={() => openProfileViewer(userId, roomId)}>
<Avatar imageSrc={avatarSrc} text={username} bgColor={colorMXID(userId)} size="small" />
</button>
</div>
);
});
roomId, avatarSrc, userId, username,
}) => (
<div className="message__avatar-container">
<button type="button" onClick={() => openProfileViewer(userId, roomId)}>
<Avatar imageSrc={avatarSrc} text={username} bgColor={colorMXID(userId)} size="small" />
</button>
</div>
));
const MessageHeader = React.memo(({
userId, username, time,
@ -597,7 +594,8 @@ function Message({
mEvent, isBodyOnly, roomTimeline, focus, time,
}) {
const [isEditing, setIsEditing] = useState(false);
const { roomId, editedTimeline, reactionTimeline } = roomTimeline;
const roomId = mEvent.getRoomId();
const { editedTimeline, reactionTimeline } = roomTimeline ?? {};
const className = ['message', (isBodyOnly ? 'message--body-only' : 'message--full')];
if (focus) className.push('message--focus');
@ -606,7 +604,8 @@ function Message({
const msgType = content?.msgtype;
const senderId = mEvent.getSender();
let { body } = content;
const username = getUsernameOfRoomMember(mEvent.sender);
const username = mEvent.sender ? getUsernameOfRoomMember(mEvent.sender) : getUsername(senderId);
const avatarSrc = mEvent.sender?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop') ?? null;
const edit = useCallback(() => {
setIsEditing(true);
@ -619,8 +618,10 @@ function Message({
if (msgType === 'm.emote') className.push('message--type-emote');
let isCustomHTML = content.format === 'org.matrix.custom.html';
const isEdited = editedTimeline.has(eventId);
const haveReactions = reactionTimeline.has(eventId) || !!mEvent.getServerAggregatedRelation('m.annotation');
const isEdited = roomTimeline ? editedTimeline.has(eventId) : false;
const haveReactions = roomTimeline
? reactionTimeline.has(eventId) || !!mEvent.getServerAggregatedRelation('m.annotation')
: false;
const isReply = !!mEvent.replyEventId;
let customHTML = isCustomHTML ? content.formatted_body : null;
@ -640,13 +641,20 @@ function Message({
{
isBodyOnly
? <div className="message__avatar-container" />
: <MessageAvatar roomId={roomId} mEvent={mEvent} userId={senderId} username={username} />
: (
<MessageAvatar
roomId={roomId}
avatarSrc={avatarSrc}
userId={senderId}
username={username}
/>
)
}
<div className="message__main-container">
{!isBodyOnly && (
<MessageHeader userId={senderId} username={username} time={time} />
)}
{isReply && (
{roomTimeline && isReply && (
<MessageReplyWrapper
roomTimeline={roomTimeline}
eventId={mEvent.replyEventId}
@ -676,7 +684,7 @@ function Message({
{haveReactions && (
<MessageReactionGroup roomTimeline={roomTimeline} mEvent={mEvent} />
)}
{!isEditing && (
{roomTimeline && !isEditing && (
<MessageOptions
roomTimeline={roomTimeline}
mEvent={mEvent}
@ -691,11 +699,12 @@ function Message({
Message.defaultProps = {
isBodyOnly: false,
focus: false,
roomTimeline: null,
};
Message.propTypes = {
mEvent: PropTypes.shape({}).isRequired,
isBodyOnly: PropTypes.bool,
roomTimeline: PropTypes.shape({}).isRequired,
roomTimeline: PropTypes.shape({}),
focus: PropTypes.bool,
time: PropTypes.string.isRequired,
};

View file

@ -0,0 +1,193 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './RoomSearch.scss';
import dateFormat from 'dateformat';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import { selectRoom } from '../../../client/action/navigation';
import Text from '../../atoms/text/Text';
import RawIcon from '../../atoms/system-icons/RawIcon';
import Button from '../../atoms/button/Button';
import Input from '../../atoms/input/Input';
import Spinner from '../../atoms/spinner/Spinner';
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
import { Message } from '../message/Message';
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
import { useStore } from '../../hooks/useStore';
const roomIdToBackup = new Map();
function useRoomSearch(roomId) {
const [searchData, setSearchData] = useState(roomIdToBackup.get(roomId) ?? null);
const [status, setStatus] = useState({
type: cons.status.PRE_FLIGHT,
term: null,
});
const mountStore = useStore(roomId);
const mx = initMatrix.matrixClient;
useEffect(() => mountStore.setItem(true), [roomId]);
useEffect(() => {
if (searchData?.results?.length > 0) {
roomIdToBackup.set(roomId, searchData);
} else {
roomIdToBackup.delete(roomId);
}
}, [searchData]);
const search = async (term) => {
setSearchData(null);
if (term === '') {
setStatus({ type: cons.status.PRE_FLIGHT, term: null });
return;
}
setStatus({ type: cons.status.IN_FLIGHT, term });
const body = {
search_categories: {
room_events: {
search_term: term,
filter: {
limit: 10,
rooms: [roomId],
},
order_by: 'recent',
event_context: {
before_limit: 0,
after_limit: 0,
include_profile: true,
},
},
},
};
try {
const res = await mx.search({ body });
const data = mx.processRoomEventsSearch({
_query: body,
results: [],
highlights: [],
}, res);
if (!mountStore.getItem()) return;
setStatus({ type: cons.status.SUCCESS, term });
setSearchData(data);
if (!mountStore.getItem()) return;
} catch (error) {
setSearchData(null);
setStatus({ type: cons.status.ERROR, term });
}
};
const paginate = async () => {
if (searchData === null) return;
const term = searchData._query.search_categories.room_events.search_term;
setStatus({ type: cons.status.IN_FLIGHT, term });
try {
const data = await mx.backPaginateRoomEventsSearch(searchData);
if (!mountStore.getItem()) return;
setStatus({ type: cons.status.SUCCESS, term });
setSearchData(data);
} catch (error) {
if (!mountStore.getItem()) return;
setSearchData(null);
setStatus({ type: cons.status.ERROR, term });
}
};
return [searchData, search, paginate, status];
}
function RoomSearch({ roomId }) {
const [searchData, search, paginate, status] = useRoomSearch(roomId);
const searchTerm = searchData?._query.search_categories.room_events.search_term ?? '';
const handleSearch = (e) => {
e.preventDefault();
const searchTermInput = e.target.elements['room-search-input'];
const term = searchTermInput.value.trim();
search(term);
};
const renderTimeline = (timeline) => (
<div className="room-search__result-item" key={timeline[0].getId()}>
{ timeline.map((mEvent) => {
const time = dateFormat(mEvent.getDate(), 'dd/mm/yyyy - hh:MM TT');
const id = mEvent.getId();
return (
<React.Fragment key={id}>
<Message
mEvent={mEvent}
isBodyOnly={false}
time={time}
/>
<Button onClick={() => selectRoom(roomId, id)}>View</Button>
</React.Fragment>
);
})}
</div>
);
return (
<div className="room-search">
<form className="room-search__form" onSubmit={handleSearch}>
<MenuHeader>Room search</MenuHeader>
<div>
<Input
placeholder="Search for keywords"
name="room-search-input"
/>
<Button iconSrc={SearchIC} variant="primary" type="submit">Search</Button>
</div>
{searchData?.results.length > 0 && (
<Text>{`${searchData.count} results for "${searchTerm}"`}</Text>
)}
</form>
{searchData === null && (
<div className="room-search__help">
{status.type === cons.status.IN_FLIGHT && <Spinner />}
{status.type === cons.status.IN_FLIGHT && <Text>Searching room messages...</Text>}
{status.type === cons.status.PRE_FLIGHT && <RawIcon src={SearchIC} size="large" />}
{status.type === cons.status.PRE_FLIGHT && <Text>Search room messages</Text>}
{status.type === cons.status.ERROR && <Text>Failed to search messages</Text>}
</div>
)}
{searchData?.results.length === 0 && (
<div className="room-search__help">
<Text>No result found</Text>
</div>
)}
{searchData?.results.length > 0 && (
<>
<div className="room-search__content">
{searchData.results.map((searchResult) => {
const { timeline } = searchResult.context;
return renderTimeline(timeline);
})}
</div>
{searchData?.next_batch && (
<div className="room-search__more">
{status.type !== cons.status.IN_FLIGHT && (
<Button onClick={paginate}>Load more</Button>
)}
{status.type === cons.status.IN_FLIGHT && <Spinner />}
</div>
)}
</>
)}
</div>
);
}
RoomSearch.propTypes = {
roomId: PropTypes.string.isRequired,
};
export default RoomSearch;

View file

@ -0,0 +1,62 @@
@use '../../partials/flex';
@use '../../partials/dir';
.room-search {
&__form {
& div:nth-child(2) {
display: flex;
align-items: flex-end;
padding: var(--sp-normal);;
& .input-container {
@extend .cp-fx__item-one;
@include dir.side(margin, 0, var(--sp-normal));
}
& button {
height: 46px;
}
}
& .context-menu__header {
margin-bottom: 0;
}
& > .text {
padding: 0 var(--sp-normal) var(--sp-tight);
}
}
&__help {
height: 248px;
@extend .cp-fx__column--c-c;
& .ic-raw {
opacity: .5;
}
.text {
margin-top: var(--sp-normal);
}
}
&__more {
margin-bottom: var(--sp-normal);
@extend .cp-fx__row--c-c;
button {
width: 100%;
}
}
&__result-item {
padding: var(--sp-tight) var(--sp-normal);
display: flex;
align-items: flex-start;
.message {
@include dir.side(margin, 0, var(--sp-normal));
@extend .cp-fx__item-one;
padding: 0;
&:hover {
background-color: transparent;
}
& .message__time {
flex: 0;
}
}
}
}

View file

@ -14,6 +14,7 @@ import ScrollView from '../../atoms/scroll/ScrollView';
import Tabs from '../../atoms/tabs/Tabs';
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
import RoomProfile from '../../molecules/room-profile/RoomProfile';
import RoomSearch from '../../molecules/room-search/RoomSearch';
import RoomNotification from '../../molecules/room-notification/RoomNotification';
import RoomVisibility from '../../molecules/room-visibility/RoomVisibility';
import RoomAliases from '../../molecules/room-aliases/RoomAliases';
@ -151,6 +152,7 @@ function RoomSettings({ roomId }) {
/>
<div className="room-settings__cards-wrapper">
{selectedTab.text === tabText.GENERAL && <GeneralSettings roomId={roomId} />}
{selectedTab.text === tabText.SEARCH && <RoomSearch roomId={roomId} />}
{selectedTab.text === tabText.PERMISSIONS && <RoomPermissions roomId={roomId} />}
{selectedTab.text === tabText.SECURITY && <SecuritySettings roomId={roomId} />}
</div>

View file

@ -46,6 +46,9 @@
}
}
.room-settings .room-permissions__card {
.room-settings .room-permissions__card,
.room-settings .room-search__form,
.room-settings .room-search__help,
.room-settings .room-search__result-item {
@extend .room-settings__card;
}

View file

@ -73,8 +73,6 @@ class Navigation extends EventEmitter {
this.emit(cons.events.navigation.SPACE_SELECTED, this.selectedSpaceId);
},
[cons.actions.navigation.SELECT_ROOM]: () => {
if (this.selectedRoomId === action.roomId) return;
const prevSelectedRoomId = this.selectedRoomId;
this.selectedRoomId = action.roomId;
this.removeRecentRoom(prevSelectedRoomId);