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(({
|
||||
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,
|
||||
};
|
||||
|
|
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 { 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>
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue