/* Copyright 2024 New Vector Ltd. Copyright 2019-2023 The Matrix.org Foundation C.I.C. Copyright 2018 New Vector Ltd Copyright 2017 Vector Creations Ltd Copyright 2017 New Vector Ltd Copyright 2015, 2016 OpenMarket Ltd SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ import React from "react"; import classNames from "classnames"; import { NotificationCountType, Room, RoomEvent, ThreadEvent } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; import HeaderButton from "./HeaderButton"; import HeaderButtons, { HeaderKind } from "./HeaderButtons"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; import { ActionPayload } from "../../../dispatcher/payloads"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import { showThreadPanel } from "../../../dispatcher/dispatch-actions/threads"; import SettingsStore from "../../../settings/SettingsStore"; import { RoomNotificationStateStore, UPDATE_STATUS_INDICATOR, } from "../../../stores/notifications/RoomNotificationStateStore"; import { NotificationLevel } from "../../../stores/notifications/NotificationLevel"; import { SummarizedNotificationState } from "../../../stores/notifications/SummarizedNotificationState"; import PosthogTrackers from "../../../PosthogTrackers"; import { ButtonEvent } from "../elements/AccessibleButton"; import { doesRoomOrThreadHaveUnreadMessages } from "../../../Unread"; import { usePinnedEvents, useReadPinnedEvents } from "../../../hooks/usePinnedEvents"; const ROOM_INFO_PHASES = [ RightPanelPhases.RoomSummary, RightPanelPhases.Widget, RightPanelPhases.FilePanel, RightPanelPhases.RoomMemberList, RightPanelPhases.RoomMemberInfo, RightPanelPhases.EncryptionPanel, RightPanelPhases.Room3pidMemberInfo, ]; interface IUnreadIndicatorProps { color?: NotificationLevel; } const UnreadIndicator: React.FC = ({ color }) => { if (color === NotificationLevel.None) { return null; } const classes = classNames({ mx_Indicator: true, mx_LegacyRoomHeader_button_unreadIndicator: true, mx_Indicator_activity: color === NotificationLevel.Activity, mx_Indicator_notification: color === NotificationLevel.Notification, mx_Indicator_highlight: color === NotificationLevel.Highlight, }); return ( <>
); }; interface IHeaderButtonProps { room: Room; isHighlighted: boolean; onClick: () => void; } const PinnedMessagesHeaderButton: React.FC = ({ room, isHighlighted, onClick }) => { const pinnedEvents = usePinnedEvents(room); const readPinnedEvents = useReadPinnedEvents(room); if (!pinnedEvents?.length) return null; let unreadIndicator; if (pinnedEvents.some((id) => !readPinnedEvents.has(id))) { unreadIndicator = ; } return ( {unreadIndicator} ); }; const TimelineCardHeaderButton: React.FC = ({ room, isHighlighted, onClick }) => { let unreadIndicator; const color = RoomNotificationStateStore.instance.getRoomState(room).level; switch (color) { case NotificationLevel.Activity: case NotificationLevel.Notification: case NotificationLevel.Highlight: unreadIndicator = ; } return ( {unreadIndicator} ); }; interface IProps { room?: Room; excludedRightPanelPhaseButtons?: Array; } /** * @deprecated will be removed as part of 'feature_new_room_decoration_ui' */ export default class LegacyRoomHeaderButtons extends HeaderButtons { private static readonly THREAD_PHASES = [RightPanelPhases.ThreadPanel, RightPanelPhases.ThreadView]; private globalNotificationState: SummarizedNotificationState; public constructor(props: IProps) { super(props, HeaderKind.Room); this.globalNotificationState = RoomNotificationStateStore.instance.globalState; } public componentDidMount(): void { super.componentDidMount(); // Notification badge may change if the notification counts from the // server change, if a new thread is created or updated, or if a // receipt is sent in the thread. this.props.room?.on(RoomEvent.UnreadNotifications, this.onNotificationUpdate); this.props.room?.on(RoomEvent.Receipt, this.onNotificationUpdate); this.props.room?.on(RoomEvent.Timeline, this.onNotificationUpdate); this.props.room?.on(RoomEvent.Redaction, this.onNotificationUpdate); this.props.room?.on(RoomEvent.LocalEchoUpdated, this.onNotificationUpdate); this.props.room?.on(RoomEvent.MyMembership, this.onNotificationUpdate); this.props.room?.on(ThreadEvent.New, this.onNotificationUpdate); this.props.room?.on(ThreadEvent.Update, this.onNotificationUpdate); this.onNotificationUpdate(); RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatus); } public componentWillUnmount(): void { super.componentWillUnmount(); this.props.room?.off(RoomEvent.UnreadNotifications, this.onNotificationUpdate); this.props.room?.off(RoomEvent.Receipt, this.onNotificationUpdate); this.props.room?.off(RoomEvent.Timeline, this.onNotificationUpdate); this.props.room?.off(RoomEvent.Redaction, this.onNotificationUpdate); this.props.room?.off(RoomEvent.LocalEchoUpdated, this.onNotificationUpdate); this.props.room?.off(RoomEvent.MyMembership, this.onNotificationUpdate); this.props.room?.off(ThreadEvent.New, this.onNotificationUpdate); this.props.room?.off(ThreadEvent.Update, this.onNotificationUpdate); RoomNotificationStateStore.instance.off(UPDATE_STATUS_INDICATOR, this.onUpdateStatus); } private onNotificationUpdate = (): void => { // console.log // XXX: why don't we read from this.state.threadNotificationLevel in the render methods? this.setState({ threadNotificationLevel: this.notificationLevel, }); }; private get notificationLevel(): NotificationLevel { switch (this.props.room?.threadsAggregateNotificationType) { case NotificationCountType.Highlight: return NotificationLevel.Highlight; case NotificationCountType.Total: return NotificationLevel.Notification; } // We don't have any notified messages, but we might have unread messages. Let's // find out. for (const thread of this.props.room!.getThreads()) { // If the current thread has unread messages, we're done. if (doesRoomOrThreadHaveUnreadMessages(thread)) { return NotificationLevel.Activity; } } // Otherwise, no notification color. return NotificationLevel.None; } private onUpdateStatus = (notificationState: SummarizedNotificationState): void => { // XXX: why don't we read from this.state.globalNotificationCount in the render methods? this.globalNotificationState = notificationState; this.setState({ globalNotificationLevel: notificationState.level, }); }; protected onAction(payload: ActionPayload): void {} private onRoomSummaryClicked = (): void => { // use roomPanelPhase rather than this.state.phase as it remembers the latest one if we close const currentPhase = RightPanelStore.instance.currentCard.phase; if (currentPhase && ROOM_INFO_PHASES.includes(currentPhase)) { if (this.state.phase === currentPhase) { RightPanelStore.instance.showOrHidePhase(currentPhase); } else { RightPanelStore.instance.showOrHidePhase(currentPhase, RightPanelStore.instance.currentCard.state); } } else { // This toggles for us, if needed RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomSummary); } }; private onNotificationsClicked = (): void => { // This toggles for us, if needed RightPanelStore.instance.showOrHidePhase(RightPanelPhases.NotificationPanel); }; private onPinnedMessagesClicked = (): void => { // This toggles for us, if needed RightPanelStore.instance.showOrHidePhase(RightPanelPhases.PinnedMessages); }; private onTimelineCardClicked = (): void => { RightPanelStore.instance.showOrHidePhase(RightPanelPhases.Timeline); }; private onThreadsPanelClicked = (ev: ButtonEvent): void => { if (this.state.phase && LegacyRoomHeaderButtons.THREAD_PHASES.includes(this.state.phase)) { RightPanelStore.instance.togglePanel(this.props.room?.roomId ?? null); } else { showThreadPanel(); PosthogTrackers.trackInteraction("WebRoomHeaderButtonsThreadsButton", ev); } }; public renderButtons(): JSX.Element { if (!this.props.room) { return <>; } const rightPanelPhaseButtons: Map = new Map(); if (SettingsStore.getValue("feature_pinning")) { rightPanelPhaseButtons.set( RightPanelPhases.PinnedMessages, , ); } rightPanelPhaseButtons.set( RightPanelPhases.Timeline, , ); rightPanelPhaseButtons.set( RightPanelPhases.ThreadPanel, NotificationLevel.None} > , ); if (this.state.notificationsEnabled) { rightPanelPhaseButtons.set( RightPanelPhases.NotificationPanel, {this.globalNotificationState.level === NotificationLevel.Highlight ? ( ) : null} , ); } rightPanelPhaseButtons.set( RightPanelPhases.RoomSummary, , ); return ( <> {Array.from(rightPanelPhaseButtons.keys()).map((phase) => this.props.excludedRightPanelPhaseButtons?.includes(phase) ? null : rightPanelPhaseButtons.get(phase), )} ); } }