Add ability to search room messages
Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
parent
eddba3c652
commit
dcef08009d
6 changed files with 289 additions and 22 deletions
|
@ -52,17 +52,14 @@ function PlaceholderMessage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageAvatar = React.memo(({
|
const MessageAvatar = React.memo(({
|
||||||
roomId, mEvent, userId, username,
|
roomId, avatarSrc, userId, username,
|
||||||
}) => {
|
}) => (
|
||||||
const avatarSrc = mEvent.sender.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop');
|
<div className="message__avatar-container">
|
||||||
return (
|
<button type="button" onClick={() => openProfileViewer(userId, roomId)}>
|
||||||
<div className="message__avatar-container">
|
<Avatar imageSrc={avatarSrc} text={username} bgColor={colorMXID(userId)} size="small" />
|
||||||
<button type="button" onClick={() => openProfileViewer(userId, roomId)}>
|
</button>
|
||||||
<Avatar imageSrc={avatarSrc} text={username} bgColor={colorMXID(userId)} size="small" />
|
</div>
|
||||||
</button>
|
));
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const MessageHeader = React.memo(({
|
const MessageHeader = React.memo(({
|
||||||
userId, username, time,
|
userId, username, time,
|
||||||
|
@ -597,7 +594,8 @@ function Message({
|
||||||
mEvent, isBodyOnly, roomTimeline, focus, time,
|
mEvent, isBodyOnly, roomTimeline, focus, time,
|
||||||
}) {
|
}) {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
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')];
|
const className = ['message', (isBodyOnly ? 'message--body-only' : 'message--full')];
|
||||||
if (focus) className.push('message--focus');
|
if (focus) className.push('message--focus');
|
||||||
|
@ -606,7 +604,8 @@ function Message({
|
||||||
const msgType = content?.msgtype;
|
const msgType = content?.msgtype;
|
||||||
const senderId = mEvent.getSender();
|
const senderId = mEvent.getSender();
|
||||||
let { body } = content;
|
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(() => {
|
const edit = useCallback(() => {
|
||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
|
@ -619,8 +618,10 @@ function Message({
|
||||||
if (msgType === 'm.emote') className.push('message--type-emote');
|
if (msgType === 'm.emote') className.push('message--type-emote');
|
||||||
|
|
||||||
let isCustomHTML = content.format === 'org.matrix.custom.html';
|
let isCustomHTML = content.format === 'org.matrix.custom.html';
|
||||||
const isEdited = editedTimeline.has(eventId);
|
const isEdited = roomTimeline ? editedTimeline.has(eventId) : false;
|
||||||
const haveReactions = reactionTimeline.has(eventId) || !!mEvent.getServerAggregatedRelation('m.annotation');
|
const haveReactions = roomTimeline
|
||||||
|
? reactionTimeline.has(eventId) || !!mEvent.getServerAggregatedRelation('m.annotation')
|
||||||
|
: false;
|
||||||
const isReply = !!mEvent.replyEventId;
|
const isReply = !!mEvent.replyEventId;
|
||||||
let customHTML = isCustomHTML ? content.formatted_body : null;
|
let customHTML = isCustomHTML ? content.formatted_body : null;
|
||||||
|
|
||||||
|
@ -640,13 +641,20 @@ function Message({
|
||||||
{
|
{
|
||||||
isBodyOnly
|
isBodyOnly
|
||||||
? <div className="message__avatar-container" />
|
? <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">
|
<div className="message__main-container">
|
||||||
{!isBodyOnly && (
|
{!isBodyOnly && (
|
||||||
<MessageHeader userId={senderId} username={username} time={time} />
|
<MessageHeader userId={senderId} username={username} time={time} />
|
||||||
)}
|
)}
|
||||||
{isReply && (
|
{roomTimeline && isReply && (
|
||||||
<MessageReplyWrapper
|
<MessageReplyWrapper
|
||||||
roomTimeline={roomTimeline}
|
roomTimeline={roomTimeline}
|
||||||
eventId={mEvent.replyEventId}
|
eventId={mEvent.replyEventId}
|
||||||
|
@ -676,7 +684,7 @@ function Message({
|
||||||
{haveReactions && (
|
{haveReactions && (
|
||||||
<MessageReactionGroup roomTimeline={roomTimeline} mEvent={mEvent} />
|
<MessageReactionGroup roomTimeline={roomTimeline} mEvent={mEvent} />
|
||||||
)}
|
)}
|
||||||
{!isEditing && (
|
{roomTimeline && !isEditing && (
|
||||||
<MessageOptions
|
<MessageOptions
|
||||||
roomTimeline={roomTimeline}
|
roomTimeline={roomTimeline}
|
||||||
mEvent={mEvent}
|
mEvent={mEvent}
|
||||||
|
@ -691,11 +699,12 @@ function Message({
|
||||||
Message.defaultProps = {
|
Message.defaultProps = {
|
||||||
isBodyOnly: false,
|
isBodyOnly: false,
|
||||||
focus: false,
|
focus: false,
|
||||||
|
roomTimeline: null,
|
||||||
};
|
};
|
||||||
Message.propTypes = {
|
Message.propTypes = {
|
||||||
mEvent: PropTypes.shape({}).isRequired,
|
mEvent: PropTypes.shape({}).isRequired,
|
||||||
isBodyOnly: PropTypes.bool,
|
isBodyOnly: PropTypes.bool,
|
||||||
roomTimeline: PropTypes.shape({}).isRequired,
|
roomTimeline: PropTypes.shape({}),
|
||||||
focus: PropTypes.bool,
|
focus: PropTypes.bool,
|
||||||
time: PropTypes.string.isRequired,
|
time: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
193
src/app/molecules/room-search/RoomSearch.jsx
Normal file
193
src/app/molecules/room-search/RoomSearch.jsx
Normal 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;
|
62
src/app/molecules/room-search/RoomSearch.scss
Normal file
62
src/app/molecules/room-search/RoomSearch.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import ScrollView from '../../atoms/scroll/ScrollView';
|
||||||
import Tabs from '../../atoms/tabs/Tabs';
|
import Tabs from '../../atoms/tabs/Tabs';
|
||||||
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||||
import RoomProfile from '../../molecules/room-profile/RoomProfile';
|
import RoomProfile from '../../molecules/room-profile/RoomProfile';
|
||||||
|
import RoomSearch from '../../molecules/room-search/RoomSearch';
|
||||||
import RoomNotification from '../../molecules/room-notification/RoomNotification';
|
import RoomNotification from '../../molecules/room-notification/RoomNotification';
|
||||||
import RoomVisibility from '../../molecules/room-visibility/RoomVisibility';
|
import RoomVisibility from '../../molecules/room-visibility/RoomVisibility';
|
||||||
import RoomAliases from '../../molecules/room-aliases/RoomAliases';
|
import RoomAliases from '../../molecules/room-aliases/RoomAliases';
|
||||||
|
@ -151,6 +152,7 @@ function RoomSettings({ roomId }) {
|
||||||
/>
|
/>
|
||||||
<div className="room-settings__cards-wrapper">
|
<div className="room-settings__cards-wrapper">
|
||||||
{selectedTab.text === tabText.GENERAL && <GeneralSettings roomId={roomId} />}
|
{selectedTab.text === tabText.GENERAL && <GeneralSettings roomId={roomId} />}
|
||||||
|
{selectedTab.text === tabText.SEARCH && <RoomSearch roomId={roomId} />}
|
||||||
{selectedTab.text === tabText.PERMISSIONS && <RoomPermissions roomId={roomId} />}
|
{selectedTab.text === tabText.PERMISSIONS && <RoomPermissions roomId={roomId} />}
|
||||||
{selectedTab.text === tabText.SECURITY && <SecuritySettings roomId={roomId} />}
|
{selectedTab.text === tabText.SECURITY && <SecuritySettings roomId={roomId} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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;
|
@extend .room-settings__card;
|
||||||
}
|
}
|
|
@ -73,8 +73,6 @@ class Navigation extends EventEmitter {
|
||||||
this.emit(cons.events.navigation.SPACE_SELECTED, this.selectedSpaceId);
|
this.emit(cons.events.navigation.SPACE_SELECTED, this.selectedSpaceId);
|
||||||
},
|
},
|
||||||
[cons.actions.navigation.SELECT_ROOM]: () => {
|
[cons.actions.navigation.SELECT_ROOM]: () => {
|
||||||
if (this.selectedRoomId === action.roomId) return;
|
|
||||||
|
|
||||||
const prevSelectedRoomId = this.selectedRoomId;
|
const prevSelectedRoomId = this.selectedRoomId;
|
||||||
this.selectedRoomId = action.roomId;
|
this.selectedRoomId = action.roomId;
|
||||||
this.removeRecentRoom(prevSelectedRoomId);
|
this.removeRecentRoom(prevSelectedRoomId);
|
||||||
|
|
Loading…
Reference in a new issue