{
+export default abstract class HeaderButtons extends React.Component {
private storeToken: EventSubscription;
private dispatcherRef: string;
- constructor(props: IProps, kind: HeaderKind) {
+ constructor(props: IProps & P, kind: HeaderKind) {
super(props);
const rps = RightPanelStore.getSharedInstance();
@@ -95,7 +95,7 @@ export default abstract class HeaderButtons extends React.Component
diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx
new file mode 100644
index 0000000000..a3f1f2d9df
--- /dev/null
+++ b/src/components/views/right_panel/PinnedMessagesCard.tsx
@@ -0,0 +1,176 @@
+/*
+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 } from "matrix-js-sdk/src/models/room";
+import { RoomState } from "matrix-js-sdk/src/models/room-state";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { EventType } from 'matrix-js-sdk/src/@types/event';
+
+import { _t } from "../../../languageHandler";
+import BaseCard from "./BaseCard";
+import Spinner from "../elements/Spinner";
+import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import { useEventEmitter } from "../../../hooks/useEventEmitter";
+import PinningUtils from "../../../utils/PinningUtils";
+import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
+import PinnedEventTile from "../rooms/PinnedEventTile";
+
+interface IProps {
+ room: Room;
+ onClose(): void;
+}
+
+export const usePinnedEvents = (room: Room): string[] => {
+ const [pinnedEvents, setPinnedEvents] = useState([]);
+
+ const update = useCallback((ev?: MatrixEvent) => {
+ if (!room) return;
+ if (ev && ev.getType() !== EventType.RoomPinnedEvents) return;
+ setPinnedEvents(room.currentState.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent()?.pinned || []);
+ }, [room]);
+
+ useEventEmitter(room?.currentState, "RoomState.events", update);
+ useEffect(() => {
+ update();
+ return () => {
+ setPinnedEvents([]);
+ };
+ }, [update]);
+ return pinnedEvents;
+};
+
+export const ReadPinsEventId = "im.vector.room.read_pins";
+
+export const useReadPinnedEvents = (room: Room): Set => {
+ const [readPinnedEvents, setReadPinnedEvents] = useState>(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]);
+
+ useEventEmitter(room, "Room.accountData", update);
+ useEffect(() => {
+ update();
+ return () => {
+ setReadPinnedEvents(new Set());
+ };
+ }, [update]);
+ return readPinnedEvents;
+};
+
+const useRoomState = (room: Room, mapper: (state: RoomState) => T): T => {
+ const [value, setValue] = useState(room ? mapper(room.currentState) : undefined);
+
+ const update = useCallback(() => {
+ if (!room) return;
+ setValue(mapper(room.currentState));
+ }, [room, mapper]);
+
+ useEventEmitter(room?.currentState, "RoomState.events", update);
+ useEffect(() => {
+ update();
+ return () => {
+ setValue(undefined);
+ };
+ }, [update]);
+ return value;
+};
+
+const PinnedMessagesCard = ({ room, onClose }: IProps) => {
+ const cli = useContext(MatrixClientContext);
+ 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 localEvent;
+
+ try {
+ const evJson = await cli.fetchRoomEvent(room.roomId, eventId);
+ const event = new MatrixEvent(evJson);
+ if (event.isEncrypted()) {
+ await cli.decryptEventIfNeeded(event); // TODO await?
+ }
+ if (event && PinningUtils.isPinnable(event)) {
+ return event;
+ }
+ } catch (err) {
+ console.error("Error looking up pinned event " + eventId + " in room " + room.roomId);
+ console.error(err);
+ }
+ return null;
+ });
+
+ return Promise.all(promises);
+ }, [cli, room, pinnedEventIds], null);
+
+ let content;
+ if (!pinnedEvents) {
+ content = ;
+ } else if (pinnedEvents.length > 0) {
+ let onUnpinClicked;
+ if (canUnpin) {
+ 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 => (
+
+ ));
+ } else {
+ content =
+
{_t("You’re all caught up")}
+
{_t("You have no visible notifications.")}
+
;
+ }
+
+ return { _t("Pinned messages") }}
+ className="mx_PinnedMessagesCard"
+ onClose={onClose}
+ >
+ { content }
+ ;
+};
+
+export default PinnedMessagesCard;
diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx
index 0571622e64..54e18e4529 100644
--- a/src/components/views/right_panel/RoomHeaderButtons.tsx
+++ b/src/components/views/right_panel/RoomHeaderButtons.tsx
@@ -18,15 +18,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
-import {_t} from '../../../languageHandler';
+import React from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+
+import { _t } from '../../../languageHandler';
import HeaderButton from './HeaderButton';
-import HeaderButtons, {HeaderKind} from './HeaderButtons';
-import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
-import {Action} from "../../../dispatcher/actions";
-import {ActionPayload} from "../../../dispatcher/payloads";
+import HeaderButtons, { HeaderKind } from './HeaderButtons';
+import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
+import { Action } from "../../../dispatcher/actions";
+import { ActionPayload } from "../../../dispatcher/payloads";
import RightPanelStore from "../../../stores/RightPanelStore";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { useSettingValue } from "../../../hooks/useSettings";
+import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard';
const ROOM_INFO_PHASES = [
RightPanelPhases.RoomSummary,
@@ -38,9 +42,35 @@ const ROOM_INFO_PHASES = [
RightPanelPhases.Room3pidMemberInfo,
];
+const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => {
+ const pinningEnabled = useSettingValue("feature_pinning");
+ const pinnedEvents = usePinnedEvents(pinningEnabled && room);
+ const readPinnedEvents = useReadPinnedEvents(pinningEnabled && room);
+ if (!pinningEnabled) return null;
+
+ let unreadIndicator;
+ if (pinnedEvents.some(id => !readPinnedEvents.has(id))) {
+ unreadIndicator = ;
+ }
+
+ return
+ { unreadIndicator }
+ ;
+};
+
+interface IProps {
+ room?: Room;
+}
+
@replaceableComponent("views.right_panel.RoomHeaderButtons")
-export default class RoomHeaderButtons extends HeaderButtons {
- constructor(props) {
+export default class RoomHeaderButtons extends HeaderButtons {
+ constructor(props: IProps) {
super(props, HeaderKind.Room);
}
@@ -80,24 +110,32 @@ export default class RoomHeaderButtons extends HeaderButtons {
this.setPhase(RightPanelPhases.NotificationPanel);
};
+ private onPinnedMessagesClicked = () => {
+ // This toggles for us, if needed
+ this.setPhase(RightPanelPhases.PinnedMessages);
+ };
+
public renderButtons() {
- return [
+ return <>
+
,
+ />
,
- ];
+ />
+ >;
}
}
diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx
index 88928290f4..937037f644 100644
--- a/src/components/views/right_panel/RoomSummaryCard.tsx
+++ b/src/components/views/right_panel/RoomSummaryCard.tsx
@@ -46,6 +46,7 @@ import WidgetContextMenu from "../context_menus/WidgetContextMenu";
import {useRoomMemberCount} from "../../../hooks/useRoomMembers";
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import RoomName from "../elements/RoomName";
+import UIStore from "../../../stores/UIStore";
interface IProps {
room: Room;
@@ -116,8 +117,8 @@ const AppRow: React.FC = ({ app, room }) => {
const rect = handle.current.getBoundingClientRect();
contextMenu = ;
diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index d5f67623a2..d6c97f9cf2 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -17,18 +17,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
+import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import classNames from 'classnames';
-import {MatrixClient} from 'matrix-js-sdk/src/client';
-import {RoomMember} from 'matrix-js-sdk/src/models/room-member';
-import {User} from 'matrix-js-sdk/src/models/user';
-import {Room} from 'matrix-js-sdk/src/models/room';
-import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
-import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
+import { MatrixClient } from 'matrix-js-sdk/src/client';
+import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
+import { User } from 'matrix-js-sdk/src/models/user';
+import { Room } from 'matrix-js-sdk/src/models/room';
+import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
+import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
+import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import dis from '../../../dispatcher/dispatcher';
import Modal from '../../../Modal';
-import {_t} from '../../../languageHandler';
+import { _t } from '../../../languageHandler';
import createRoom, { findDMForUser, privateShouldBeEncrypted } from '../../../createRoom';
import DMRoomMap from '../../../utils/DMRoomMap';
import AccessibleButton from '../elements/AccessibleButton';
@@ -39,18 +40,18 @@ import MultiInviter from "../../../utils/MultiInviter";
import GroupStore from "../../../stores/GroupStore";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import E2EIcon from "../rooms/E2EIcon";
-import {useEventEmitter} from "../../../hooks/useEventEmitter";
-import {textualPowerLevel} from '../../../Roles';
+import { useEventEmitter } from "../../../hooks/useEventEmitter";
+import { textualPowerLevel } from '../../../Roles';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
-import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
+import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
import EncryptionPanel from "./EncryptionPanel";
-import {useAsyncMemo} from '../../../hooks/useAsyncMemo';
-import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification';
-import {Action} from "../../../dispatcher/actions";
+import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
+import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification';
+import { Action } from "../../../dispatcher/actions";
import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog";
-import {useIsEncrypted} from "../../../hooks/useIsEncrypted";
+import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
import BaseCard from "./BaseCard";
-import {E2EStatus} from "../../../utils/ShieldUtils";
+import { E2EStatus } from "../../../utils/ShieldUtils";
import ImageView from "../elements/ImageView";
import Spinner from "../elements/Spinner";
import PowerSelector from "../elements/PowerSelector";
@@ -65,7 +66,8 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
import RoomAvatar from "../avatars/RoomAvatar";
import RoomName from "../elements/RoomName";
-import {mediaFromMxc} from "../../../customisations/Media";
+import { mediaFromMxc } from "../../../customisations/Media";
+import UIStore from "../../../stores/UIStore";
export interface IDevice {
deviceId: string;
@@ -513,9 +515,6 @@ export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
} else {
setPowerLevels({});
}
- return () => {
- setPowerLevels({});
- };
}, [room]);
useEventEmitter(cli, "RoomState.events", update);
@@ -1307,7 +1306,7 @@ const BasicUserInfo: React.FC<{
}
if (pendingUpdateCount > 0) {
- spinner = ;
+ spinner = ;
}
let memberDetails;
@@ -1448,8 +1447,8 @@ const UserInfoHeader: React.FC<{
;
}
-interface IPropsWithEncryptionPanel extends React.ComponentProps {
- user: Member;
- groupId: void;
- room: Room;
- phase: RightPanelPhases.EncryptionPanel;
- onClose(): void;
-}
-
-type Props = IProps | IPropsWithEncryptionPanel;
-
-const UserInfo: React.FC = ({
+const UserInfo: React.FC = ({
user,
groupId,
room,
diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx
index 56e522e206..d7493e0512 100644
--- a/src/components/views/right_panel/WidgetCard.tsx
+++ b/src/components/views/right_panel/WidgetCard.tsx
@@ -30,6 +30,7 @@ import { Action } from "../../../dispatcher/actions";
import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
+import UIStore from "../../../stores/UIStore";
interface IProps {
room: Room;
@@ -65,7 +66,7 @@ const WidgetCard: React.FC = ({ room, widgetId, onClose }) => {
contextMenu = (
{
private suppressReadReceiptAnimation: boolean;
private isListeningForReceipts: boolean;
+ private ref: React.RefObject;
private tile = React.createRef();
private replyThread = React.createRef();
@@ -322,6 +331,8 @@ export default class EventTile extends React.Component {
previouslyRequestedKeys: false,
// The Relations model from the JS SDK for reactions to `mxEvent`
reactions: this.getReactions(),
+
+ hover: false,
};
// don't do RR animations until we are mounted
@@ -333,6 +344,8 @@ export default class EventTile extends React.Component {
// to determine if we've already subscribed and use a combination of other flags to find
// out if we should even be subscribed at all.
this.isListeningForReceipts = false;
+
+ this.ref = React.createRef();
}
/**
@@ -631,7 +644,7 @@ export default class EventTile extends React.Component {
// return early if there are no read receipts
if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
- return ();
+ return null;
}
const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
@@ -640,6 +653,11 @@ export default class EventTile extends React.Component {
let left = 0;
const receipts = this.props.readReceipts || [];
+
+ if (receipts.length === 0) {
+ return null;
+ }
+
for (let i = 0; i < receipts.length; ++i) {
const receipt = receipts[i];
@@ -690,10 +708,14 @@ export default class EventTile extends React.Component {
}
}
- return
- { remText }
- { avatars }
- ;
+ return (
+
+
+ { remText }
+ { avatars }
+
+
+ )
}
onSenderProfileClick = event => {
@@ -790,13 +812,6 @@ export default class EventTile extends React.Component {
return null;
}
const eventId = this.props.mxEvent.getId();
- if (!eventId) {
- // XXX: Temporary diagnostic logging for https://github.com/vector-im/element-web/issues/11120
- console.error("EventTile attempted to get relations for an event without an ID");
- // Use event's special `toJSON` method to log key data.
- console.log(JSON.stringify(this.props.mxEvent, null, 4));
- console.trace("Stacktrace for https://github.com/vector-im/element-web/issues/11120");
- }
return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction");
};
@@ -960,7 +975,8 @@ export default class EventTile extends React.Component {
onFocusChange={this.onActionBarFocusChange}
/> : undefined;
- const timestamp = this.props.mxEvent.getTs() ?
+ const showTimestamp = this.props.mxEvent.getTs() && (this.props.alwaysShowTimestamps || this.state.hover);
+ const timestamp = showTimestamp ?
: null;
const keyRequestHelpText =
@@ -1023,11 +1039,7 @@ export default class EventTile extends React.Component {
let msgOption;
if (this.props.showReadReceipts) {
const readAvatars = this.getReadAvatars();
- msgOption = (
-
- { readAvatars }
-
- );
+ msgOption = readAvatars;
}
switch (this.props.tileShape) {
@@ -1131,11 +1143,20 @@ export default class EventTile extends React.Component {
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
return (
-
- { ircTimestamp }
- { sender }
- { ircPadlock }
-
+ React.createElement(this.props.as || "div", {
+ "ref": this.ref,
+ "className": classes,
+ "tabIndex": -1,
+ "aria-live": ariaLive,
+ "aria-atomic": "true",
+ "data-scroll-tokens": this.props["data-scroll-tokens"],
+ "onMouseEnter": () => this.setState({ hover: true }),
+ "onMouseLeave": () => this.setState({ hover: false }),
+ }, [
+ ircTimestamp,
+ sender,
+ ircPadlock,
+
{ groupTimestamp }
{ groupPadlock }
{ thread }
@@ -1152,16 +1173,12 @@ export default class EventTile extends React.Component {
{ keyRequestInfo }
{ reactionsRow }
{ actionBar }
-
- {msgOption}
- {
- // The avatar goes after the event tile as it's absolutely positioned to be over the
- // event tile line, so needs to be later in the DOM so it appears on top (this avoids
- // the need for further z-indexing chaos)
- }
- { avatar }
-
- );
+
,
+ msgOption,
+ avatar,
+
+ ])
+ )
}
}
}
diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js
index 55fe650b7b..cb50f0fff3 100644
--- a/src/components/views/rooms/MemberList.js
+++ b/src/components/views/rooms/MemberList.js
@@ -238,6 +238,8 @@ export default class MemberList extends React.Component {
member.user = cli.getUser(member.userId);
}
+ member.sortName = (member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, "");
+
// XXX: this user may have no lastPresenceTs value!
// the right solution here is to fix the race rather than leave it as 0
});
@@ -252,6 +254,8 @@ export default class MemberList extends React.Component {
m.membership === 'join' || m.membership === 'invite'
);
});
+ const language = SettingsStore.getValue("language");
+ this.collator = new Intl.Collator(language, { sensitivity: 'base', usePunctuation: true });
filteredAndSortedMembers.sort(this.memberSort);
return filteredAndSortedMembers;
}
@@ -351,13 +355,7 @@ export default class MemberList extends React.Component {
}
// Fourth by name (alphabetical)
- const nameA = (memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name).replace(SORT_REGEX, "");
- const nameB = (memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name).replace(SORT_REGEX, "");
- // console.log(`Comparing userA_name=${nameA} against userB_name=${nameB} - returning`);
- return nameA.localeCompare(nameB, {
- ignorePunctuation: true,
- sensitivity: "base",
- });
+ return this.collator.compare(memberA.sortName, memberB.sortName);
};
onSearchQueryChanged = searchQuery => {
@@ -422,7 +420,7 @@ export default class MemberList extends React.Component {
} else {
// Is a 3pid invite
return this._onPending3pidInviteClick(m)} />;
+ onClick={() => this._onPending3pidInviteClick(m)} />;
}
});
}
@@ -484,10 +482,10 @@ export default class MemberList extends React.Component {
if (this._getChildCountInvited() > 0) {
invitedHeader = { _t("Invited") }
;
invitedSection = ;
+ createOverflowElement={this._createOverflowTileInvited}
+ getChildren={this._getChildrenInvited}
+ getChildCount={this._getChildCountInvited}
+ />;
}
const footer = (
@@ -520,9 +518,9 @@ export default class MemberList extends React.Component {
>
+ createOverflowElement={this._createOverflowTileJoined}
+ getChildren={this._getChildrenJoined}
+ getChildCount={this._getChildCountJoined} />
{ invitedHeader }
{ invitedSection }
diff --git a/src/components/views/rooms/PinnedEventTile.js b/src/components/views/rooms/PinnedEventTile.js
deleted file mode 100644
index 78cf422cc6..0000000000
--- a/src/components/views/rooms/PinnedEventTile.js
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
-Copyright 2017 Travis Ralston
-
-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 from "react";
-import PropTypes from 'prop-types';
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
-import dis from "../../../dispatcher/dispatcher";
-import AccessibleButton from "../elements/AccessibleButton";
-import MessageEvent from "../messages/MessageEvent";
-import MemberAvatar from "../avatars/MemberAvatar";
-import { _t } from '../../../languageHandler';
-import {formatFullDate} from '../../../DateUtils';
-import {replaceableComponent} from "../../../utils/replaceableComponent";
-
-@replaceableComponent("views.rooms.PinnedEventTile")
-export default class PinnedEventTile extends React.Component {
- static propTypes = {
- mxRoom: PropTypes.object.isRequired,
- mxEvent: PropTypes.object.isRequired,
- onUnpinned: PropTypes.func,
- };
-
- onTileClicked = () => {
- dis.dispatch({
- action: 'view_room',
- event_id: this.props.mxEvent.getId(),
- highlighted: true,
- room_id: this.props.mxEvent.getRoomId(),
- });
- };
-
- onUnpinClicked = () => {
- const pinnedEvents = this.props.mxRoom.currentState.getStateEvents("m.room.pinned_events", "");
- if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
- // Nothing to do: already unpinned
- if (this.props.onUnpinned) this.props.onUnpinned();
- } else {
- const pinned = pinnedEvents.getContent().pinned;
- const index = pinned.indexOf(this.props.mxEvent.getId());
- if (index !== -1) {
- pinned.splice(index, 1);
- MatrixClientPeg.get().sendStateEvent(this.props.mxRoom.roomId, 'm.room.pinned_events', {pinned}, '')
- .then(() => {
- if (this.props.onUnpinned) this.props.onUnpinned();
- });
- } else if (this.props.onUnpinned) this.props.onUnpinned();
- }
- };
-
- _canUnpin() {
- return this.props.mxRoom.currentState.mayClientSendStateEvent('m.room.pinned_events', MatrixClientPeg.get());
- }
-
- render() {
- const sender = this.props.mxEvent.getSender();
- // Get the latest sender profile rather than historical
- const senderProfile = this.props.mxRoom.getMember(sender);
- const avatarSize = 40;
-
- let unpinButton = null;
- if (this._canUnpin()) {
- unpinButton = (
-
-
-
- );
- }
-
- return (
-
-
-
- { _t("Jump to message") }
-
- { unpinButton }
-
-
-
-
-
-
- { senderProfile ? senderProfile.name : sender }
-
-
- { formatFullDate(new Date(this.props.mxEvent.getTs())) }
-
-
- {}} // we need to give this, apparently
- />
-
-
- );
- }
-}
diff --git a/src/components/views/rooms/PinnedEventTile.tsx b/src/components/views/rooms/PinnedEventTile.tsx
new file mode 100644
index 0000000000..774dea70c8
--- /dev/null
+++ b/src/components/views/rooms/PinnedEventTile.tsx
@@ -0,0 +1,104 @@
+/*
+Copyright 2017 Travis Ralston
+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 from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+
+import dis from "../../../dispatcher/dispatcher";
+import AccessibleButton from "../elements/AccessibleButton";
+import MessageEvent from "../messages/MessageEvent";
+import MemberAvatar from "../avatars/MemberAvatar";
+import { _t } from '../../../languageHandler';
+import { formatDate } from '../../../DateUtils';
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import { getUserNameColorClass } from "../../../utils/FormattingUtils";
+import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
+
+interface IProps {
+ room: Room;
+ event: MatrixEvent;
+ onUnpinClicked?(): void;
+}
+
+const AVATAR_SIZE = 24;
+
+@replaceableComponent("views.rooms.PinnedEventTile")
+export default class PinnedEventTile extends React.Component {
+ public static contextType = MatrixClientContext;
+
+ private onTileClicked = () => {
+ dis.dispatch({
+ action: 'view_room',
+ event_id: this.props.event.getId(),
+ highlighted: true,
+ room_id: this.props.event.getRoomId(),
+ });
+ };
+
+ render() {
+ const sender = this.props.event.getSender();
+ const senderProfile = this.props.room.getMember(sender);
+
+ let unpinButton = null;
+ if (this.props.onUnpinClicked) {
+ unpinButton = (
+
+ );
+ }
+
+ return
+
+
+
+ { senderProfile?.name || sender }
+
+
+ { unpinButton }
+
+
+ {}} // we need to give this, apparently
+ />
+
+
+
+
+ { formatDate(new Date(this.props.event.getTs())) }
+
+
+
+ { _t("View message") }
+
+
+
;
+ }
+}
diff --git a/src/components/views/rooms/PinnedEventsPanel.js b/src/components/views/rooms/PinnedEventsPanel.js
deleted file mode 100644
index 4b310dbbca..0000000000
--- a/src/components/views/rooms/PinnedEventsPanel.js
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
-Copyright 2017 Travis Ralston
-Copyright 2019 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 from "react";
-import PropTypes from 'prop-types';
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
-import AccessibleButton from "../elements/AccessibleButton";
-import PinnedEventTile from "./PinnedEventTile";
-import { _t } from '../../../languageHandler';
-import PinningUtils from "../../../utils/PinningUtils";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
-
-@replaceableComponent("views.rooms.PinnedEventsPanel")
-export default class PinnedEventsPanel extends React.Component {
- static propTypes = {
- // The Room from the js-sdk we're going to show pinned events for
- room: PropTypes.object.isRequired,
-
- onCancelClick: PropTypes.func,
- };
-
- state = {
- loading: true,
- };
-
- componentDidMount() {
- this._updatePinnedMessages();
- MatrixClientPeg.get().on("RoomState.events", this._onStateEvent);
- }
-
- componentWillUnmount() {
- if (MatrixClientPeg.get()) {
- MatrixClientPeg.get().removeListener("RoomState.events", this._onStateEvent);
- }
- }
-
- _onStateEvent = ev => {
- if (ev.getRoomId() === this.props.room.roomId && ev.getType() === "m.room.pinned_events") {
- this._updatePinnedMessages();
- }
- };
-
- _updatePinnedMessages = () => {
- const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
- if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
- this.setState({ loading: false, pinned: [] });
- } else {
- const promises = [];
- const cli = MatrixClientPeg.get();
-
- pinnedEvents.getContent().pinned.map((eventId) => {
- promises.push(cli.getEventTimeline(this.props.room.getUnfilteredTimelineSet(), eventId, 0).then(
- (timeline) => {
- const event = timeline.getEvents().find((e) => e.getId() === eventId);
- return {eventId, timeline, event};
- }).catch((err) => {
- console.error("Error looking up pinned event " + eventId + " in room " + this.props.room.roomId);
- console.error(err);
- return null; // return lack of context to avoid unhandled errors
- }));
- });
-
- Promise.all(promises).then((contexts) => {
- // Filter out the messages before we try to render them
- const pinned = contexts.filter((context) => PinningUtils.isPinnable(context.event));
-
- this.setState({ loading: false, pinned });
- });
- }
-
- this._updateReadState();
- };
-
- _updateReadState() {
- const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
- if (!pinnedEvents) return; // nothing to read
-
- let readStateEvents = [];
- const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
- if (readPinsEvent && readPinsEvent.getContent()) {
- readStateEvents = readPinsEvent.getContent().event_ids || [];
- }
-
- if (!readStateEvents.includes(pinnedEvents.getId())) {
- readStateEvents.push(pinnedEvents.getId());
-
- // Only keep the last 10 event IDs to avoid infinite growth
- readStateEvents = readStateEvents.reverse().splice(0, 10).reverse();
-
- MatrixClientPeg.get().setRoomAccountData(this.props.room.roomId, "im.vector.room.read_pins", {
- event_ids: readStateEvents,
- });
- }
- }
-
- _getPinnedTiles() {
- if (this.state.pinned.length === 0) {
- return ({ _t("No pinned messages.") }
);
- }
-
- return this.state.pinned.map((context) => {
- return (
-
- );
- });
- }
-
- render() {
- let tiles = { _t("Loading...") }
;
- if (this.state && !this.state.loading) {
- tiles = this._getPinnedTiles();
- }
-
- return (
-
-
-
-
-
-
{ _t("Pinned Messages") }
- { tiles }
-
-
- );
- }
-}
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js
index 9102b0386d..27eec9c35e 100644
--- a/src/components/views/rooms/RoomHeader.js
+++ b/src/components/views/rooms/RoomHeader.js
@@ -19,10 +19,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { _t } from '../../../languageHandler';
-import {MatrixClientPeg} from '../../../MatrixClientPeg';
+import { MatrixClientPeg } from '../../../MatrixClientPeg';
import RateLimitedFunc from '../../../ratelimitedfunc';
-import {CancelButton} from './SimpleRoomHeader';
+import { CancelButton } from './SimpleRoomHeader';
import SettingsStore from "../../../settings/SettingsStore";
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
import E2EIcon from './E2EIcon';
@@ -42,7 +42,6 @@ export default class RoomHeader extends React.Component {
oobData: PropTypes.object,
inRoom: PropTypes.bool,
onSettingsClick: PropTypes.func,
- onPinnedClick: PropTypes.func,
onSearchClick: PropTypes.func,
onLeaveClick: PropTypes.func,
onCancelClick: PropTypes.func,
@@ -61,14 +60,12 @@ export default class RoomHeader extends React.Component {
componentDidMount() {
const cli = MatrixClientPeg.get();
cli.on("RoomState.events", this._onRoomStateEvents);
- cli.on("Room.accountData", this._onRoomAccountData);
}
componentWillUnmount() {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.events", this._onRoomStateEvents);
- cli.removeListener("Room.accountData", this._onRoomAccountData);
}
}
@@ -81,44 +78,11 @@ export default class RoomHeader extends React.Component {
this._rateLimitedUpdate();
};
- _onRoomAccountData = (event, room) => {
- if (!this.props.room || room.roomId !== this.props.room.roomId) return;
- if (event.getType() !== "im.vector.room.read_pins") return;
-
- this._rateLimitedUpdate();
- };
-
_rateLimitedUpdate = new RateLimitedFunc(function() {
/* eslint-disable babel/no-invalid-this */
this.forceUpdate();
}, 500);
- _hasUnreadPins() {
- const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
- if (!currentPinEvent) return false;
- if (currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0) {
- return false; // no pins == nothing to read
- }
-
- const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
- if (readPinsEvent && readPinsEvent.getContent()) {
- const readStateEvents = readPinsEvent.getContent().event_ids || [];
- if (readStateEvents) {
- return !readStateEvents.includes(currentPinEvent.getId());
- }
- }
-
- // There's pins, and we haven't read any of them
- return true;
- }
-
- _hasPins() {
- const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
- if (!currentPinEvent) return false;
-
- return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0);
- }
-
_displayInfoDialogAboutScreensharing() {
Modal.createDialog(InfoDialog, {
title: _t("Screensharing has changed"),
@@ -130,7 +94,6 @@ export default class RoomHeader extends React.Component {
render() {
let searchStatus = null;
let cancelButton = null;
- let pinnedEventsButton = null;
if (this.props.onCancelClick) {
cancelButton = ;
@@ -191,24 +154,6 @@ export default class RoomHeader extends React.Component {
/>;
}
- if (this.props.onPinnedClick && SettingsStore.getValue('feature_pinning')) {
- let pinsIndicator = null;
- if (this._hasUnreadPins()) {
- pinsIndicator = ();
- } else if (this._hasPins()) {
- pinsIndicator = ();
- }
-
- pinnedEventsButton =
-
- { pinsIndicator }
- ;
- }
-
let forgetButton;
if (this.props.onForgetClick) {
forgetButton =
@@ -258,7 +203,6 @@ export default class RoomHeader extends React.Component {
{ videoCallButton }
{ voiceCallButton }
- { pinnedEventsButton }
{ forgetButton }
{ appsButton }
{ searchButton }
@@ -275,7 +219,7 @@ export default class RoomHeader extends React.Component {
{ topicElement }
{ cancelButton }
{ rightRow }
-
+