}
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,
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');
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);
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;
{
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}
{haveReactions && (
<MessageReactionGroup roomTimeline={roomTimeline} mEvent={mEvent} />
)}
- {!isEditing && (
+ {roomTimeline && !isEditing && (
<MessageOptions
roomTimeline={roomTimeline}
mEvent={mEvent}
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,
};
--- /dev/null
+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;
--- /dev/null
+@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;
+ }
+ }
+ }
+}
\ No newline at end of file
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';
/>
<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>
}
}
-.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;
}
\ No newline at end of file
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);