You've already forked matrix-react-sdk
mirror of
https://github.com/matrix-org/matrix-react-sdk.git
synced 2026-01-03 21:42:32 +03:00
* Remove duplicate declarations and add height and overflow properties Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Move mx_TimelineCard__header under mx_BaseCard_header for normalization Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Normalize mx_BaseCard_close position Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Normalize className of header - mx_BaseCard_header__ThreadPanel - mx_BaseCard_header__TimelineCard Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Normalize header's button size Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Normalize inline start header margin Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * normalize header bottom margin for PinnedMessagesCard and TimelineCard Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Normalize header declarations Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Add mixin RightPanelCard Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Move common declarations - top level Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Move common declarations - mx_BaseCard_header Remove specific declarations on PinnedMessagesCard Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Move common declarations - mx_BaseCard_back and mx_BaseCard_close Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Create a common class name - mx_BaseCard_header_title Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Create a common class name - mx_BaseCard_header_title - ThreadPanel Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Move common declarations - mx_BaseCard_header_title Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Move common declarations - span:first-of-type Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Remove redundant declarations Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Rename a variable to remove --ThreadPanel_header-button-size Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Remove class name - mx_BaseCard_header_title Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Remove mx_BaseCard_header_title--ThreadPanel and h2 declarations from PinnedMessagesCard Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Headers need Heading Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Use spacing variables Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Move common style rules of mx_ContextualMenu inside mx_BaseCard_header_title to BaseCard leaving style rules specific to ThreadPanel. Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Hide long header title with ellipsis Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Merge style rules - BaseCard_header-button-size Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Merge style rules - BaseCard_header margin-bottom Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Merge style rules - BaseCard back and close margin Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Merge style rules - BaseCard back ~ mx_BaseCard_header_title Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Merge style rules - mx_BaseCard_header_title Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Return special declarations to _ThreadPanel.scss Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Remove the mixin Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Add mx_BaseCard_header_title_button--option Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Remove redundant margin from AppTileFullWidth Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Header on mx_RoomSummaryCard - remove default declarations Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Remove default declarations from mx_UserInfo Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com> * Use variables - _BaseCard.scss Signed-off-by: Suguru Hirahara <luixxiul@users.noreply.github.com>
193 lines
7.8 KiB
TypeScript
193 lines
7.8 KiB
TypeScript
/*
|
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
import React, { useCallback, useContext, useEffect, useState } from "react";
|
|
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
import { EventType, RelationType } from 'matrix-js-sdk/src/@types/event';
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
|
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
|
|
|
import { _t } from "../../../languageHandler";
|
|
import BaseCard from "./BaseCard";
|
|
import Spinner from "../elements/Spinner";
|
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
|
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
|
|
import PinningUtils from "../../../utils/PinningUtils";
|
|
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
|
import PinnedEventTile from "../rooms/PinnedEventTile";
|
|
import { useRoomState } from "../../../hooks/useRoomState";
|
|
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
|
import { ReadPinsEventId } from "./types";
|
|
import Heading from '../typography/Heading';
|
|
|
|
interface IProps {
|
|
room: Room;
|
|
onClose(): void;
|
|
}
|
|
|
|
export const usePinnedEvents = (room: Room): string[] => {
|
|
const [pinnedEvents, setPinnedEvents] = useState<string[]>([]);
|
|
|
|
const update = useCallback((ev?: MatrixEvent) => {
|
|
if (!room) return;
|
|
if (ev && ev.getType() !== EventType.RoomPinnedEvents) return;
|
|
setPinnedEvents(room.currentState.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent()?.pinned || []);
|
|
}, [room]);
|
|
|
|
useTypedEventEmitter(room?.currentState, RoomStateEvent.Events, update);
|
|
useEffect(() => {
|
|
update();
|
|
return () => {
|
|
setPinnedEvents([]);
|
|
};
|
|
}, [update]);
|
|
return pinnedEvents;
|
|
};
|
|
|
|
export const useReadPinnedEvents = (room: Room): Set<string> => {
|
|
const [readPinnedEvents, setReadPinnedEvents] = useState<Set<string>>(new Set());
|
|
|
|
const update = useCallback((ev?: MatrixEvent) => {
|
|
if (!room) return;
|
|
if (ev && ev.getType() !== ReadPinsEventId) return;
|
|
const readPins = room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids;
|
|
setReadPinnedEvents(new Set(readPins || []));
|
|
}, [room]);
|
|
|
|
useTypedEventEmitter(room, RoomEvent.AccountData, update);
|
|
useEffect(() => {
|
|
update();
|
|
return () => {
|
|
setReadPinnedEvents(new Set());
|
|
};
|
|
}, [update]);
|
|
return readPinnedEvents;
|
|
};
|
|
|
|
const PinnedMessagesCard = ({ room, onClose }: IProps) => {
|
|
const cli = useContext(MatrixClientContext);
|
|
const roomContext = useContext(RoomContext);
|
|
const canUnpin = useRoomState(room, state => state.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli));
|
|
const pinnedEventIds = usePinnedEvents(room);
|
|
const readPinnedEvents = useReadPinnedEvents(room);
|
|
|
|
useEffect(() => {
|
|
const newlyRead = pinnedEventIds.filter(id => !readPinnedEvents.has(id));
|
|
if (newlyRead.length > 0) {
|
|
// clear out any read pinned events which no longer are pinned
|
|
cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
|
|
event_ids: pinnedEventIds,
|
|
});
|
|
}
|
|
}, [cli, room.roomId, pinnedEventIds, readPinnedEvents]);
|
|
|
|
const pinnedEvents = useAsyncMemo(() => {
|
|
const promises = pinnedEventIds.map(async eventId => {
|
|
const timelineSet = room.getUnfilteredTimelineSet();
|
|
const localEvent = timelineSet?.getTimelineForEvent(eventId)?.getEvents().find(e => e.getId() === eventId);
|
|
if (localEvent) return PinningUtils.isPinnable(localEvent) ? localEvent : null;
|
|
|
|
try {
|
|
// Fetch the event and latest edit in parallel
|
|
const [evJson, { events: [edit] }] = await Promise.all([
|
|
cli.fetchRoomEvent(room.roomId, eventId),
|
|
cli.relations(room.roomId, eventId, RelationType.Replace, null, { limit: 1 }),
|
|
]);
|
|
const event = new MatrixEvent(evJson);
|
|
if (event.isEncrypted()) {
|
|
await cli.decryptEventIfNeeded(event); // TODO await?
|
|
}
|
|
|
|
if (event && PinningUtils.isPinnable(event)) {
|
|
// Inject sender information
|
|
event.sender = room.getMember(event.getSender());
|
|
// Also inject any edits we've found
|
|
if (edit) event.makeReplaced(edit);
|
|
|
|
return event;
|
|
}
|
|
} catch (err) {
|
|
logger.error("Error looking up pinned event " + eventId + " in room " + room.roomId);
|
|
logger.error(err);
|
|
}
|
|
return null;
|
|
});
|
|
|
|
return Promise.all(promises);
|
|
}, [cli, room, pinnedEventIds], null);
|
|
|
|
let content;
|
|
if (!pinnedEvents) {
|
|
content = <Spinner />;
|
|
} else if (pinnedEvents.length > 0) {
|
|
const onUnpinClicked = async (event: MatrixEvent) => {
|
|
const pinnedEvents = room.currentState.getStateEvents(EventType.RoomPinnedEvents, "");
|
|
if (pinnedEvents?.getContent()?.pinned) {
|
|
const pinned = pinnedEvents.getContent().pinned;
|
|
const index = pinned.indexOf(event.getId());
|
|
if (index !== -1) {
|
|
pinned.splice(index, 1);
|
|
await cli.sendStateEvent(room.roomId, EventType.RoomPinnedEvents, { pinned }, "");
|
|
}
|
|
}
|
|
};
|
|
|
|
// show them in reverse, with latest pinned at the top
|
|
content = pinnedEvents.filter(Boolean).reverse().map(ev => (
|
|
<PinnedEventTile
|
|
key={ev.getId()}
|
|
event={ev}
|
|
onUnpinClicked={canUnpin ? () => onUnpinClicked(ev) : undefined}
|
|
/>
|
|
));
|
|
} else {
|
|
content = <div className="mx_PinnedMessagesCard_empty_wrapper">
|
|
<div className="mx_PinnedMessagesCard_empty">
|
|
{ /* XXX: We reuse the classes for simplicity, but deliberately not the components for non-interactivity. */ }
|
|
<div className="mx_MessageActionBar mx_PinnedMessagesCard_MessageActionBar">
|
|
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_reactButton" />
|
|
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton" />
|
|
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton" />
|
|
</div>
|
|
|
|
<Heading size="h4" className="mx_PinnedMessagesCard_empty_header">{ _t("Nothing pinned, yet") }</Heading>
|
|
{ _t("If you have permissions, open the menu on any message and select " +
|
|
"<b>Pin</b> to stick them here.", {}, {
|
|
b: sub => <b>{ sub }</b>,
|
|
}) }
|
|
</div>
|
|
</div>;
|
|
}
|
|
|
|
return <BaseCard
|
|
header={<div className="mx_BaseCard_header_title">
|
|
<Heading size="h4" className="mx_BaseCard_header_title_heading">{ _t("Pinned messages") }</Heading>
|
|
</div>}
|
|
className="mx_PinnedMessagesCard"
|
|
onClose={onClose}
|
|
>
|
|
<RoomContext.Provider value={{
|
|
...roomContext,
|
|
timelineRenderingType: TimelineRenderingType.Pinned,
|
|
}}>
|
|
{ content }
|
|
</RoomContext.Provider>
|
|
</BaseCard>;
|
|
};
|
|
|
|
export default PinnedMessagesCard;
|