This repository has been archived by the owner on Sep 11, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 833
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a banner at the top of a room to display the pinned messages
- Loading branch information
1 parent
3112878
commit a6bb865
Showing
6 changed files
with
403 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
/* | ||
* Copyright 2024 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. | ||
*/ | ||
|
||
.mx_PinnedMessageBanner { | ||
display: flex; | ||
align-items: center; | ||
justify-content: space-between; | ||
gap: var(--cpd-space-4x); | ||
/* 80px = 79px + 1px from the bottom border */ | ||
height: 79px; | ||
padding: 0 var(--cpd-space-4x); | ||
|
||
background-color: var(--cpd-color-bg-canvas-default); | ||
border-bottom: 1px solid var(--cpd-color-gray-400); | ||
|
||
/* From figma */ | ||
box-shadow: 0 var(--cpd-space-2x) var(--cpd-space-6x) calc(var(--cpd-space-2x) * -1) rgba(27, 29, 34, 0.1); | ||
|
||
.mx_PinnedMessageBanner_main { | ||
background: transparent; | ||
border: none; | ||
text-align: start; | ||
cursor: pointer; | ||
|
||
display: grid; | ||
grid-template: | ||
"indicators pinIcon title" auto | ||
"indicators pinIcon message" auto; | ||
column-gap: var(--cpd-space-2x); | ||
|
||
.mx_PinnedMessageBanner_Indicators { | ||
grid-area: indicators; | ||
display: flex; | ||
flex-direction: column; | ||
gap: var(--cpd-space-0-5x); | ||
height: 100%; | ||
|
||
.mx_PinnedMessageBanner_Indicator { | ||
width: var(--cpd-space-0-5x); | ||
background-color: var(--cpd-color-gray-600); | ||
height: 100%; | ||
} | ||
|
||
.mx_PinnedMessageBanner_Indicator--active { | ||
background-color: var(--cpd-color-icon-accent-primary); | ||
} | ||
|
||
.mx_PinnedMessageBanner_Indicator--hided { | ||
background-color: transparent; | ||
} | ||
} | ||
|
||
.mx_PinnedMessageBanner_PinIcon { | ||
grid-area: pinIcon; | ||
align-self: center; | ||
fill: var(--cpd-color-icon-secondary-alpha); | ||
} | ||
|
||
.mx_PinnedMessageBanner_title { | ||
grid-area: title; | ||
font: var(--cpd-font-body-sm-regular); | ||
color: var(--cpd-color-text-action-accent); | ||
height: 20px; | ||
|
||
.mx_PinnedMessageBanner_title_counter { | ||
font: var(--cpd-font-body-sm-semibold); | ||
} | ||
} | ||
|
||
.mx_PinnedMessageBanner_message { | ||
grid-area: message; | ||
font: var(--cpd-font-body-sm-regular); | ||
height: 20px; | ||
overflow: hidden; | ||
text-overflow: ellipsis; | ||
white-space: nowrap; | ||
} | ||
} | ||
|
||
.mx_PinnedMessageBanner_actions { | ||
white-space: nowrap; | ||
} | ||
} | ||
|
||
.mx_PinnedMessageBanner[data-single-message="true"] { | ||
/* 64px = 63px + 1px from the bottom border */ | ||
height: 63px; | ||
|
||
.mx_PinnedMessageBanner_main { | ||
grid-template: "pinIcon message" auto; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
/* | ||
* Copyright 2024 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, { JSX, useEffect, useMemo, useState } from "react"; | ||
import { Icon as PinIcon } from "@vector-im/compound-design-tokens/icons/pin-solid.svg"; | ||
import { Button } from "@vector-im/compound-web"; | ||
import { Room } from "matrix-js-sdk/src/matrix"; | ||
import classNames from "classnames"; | ||
|
||
import { useFetchPinnedEvent, usePinnedEvents } from "../../../hooks/usePinnedEvents"; | ||
import { _t } from "../../../languageHandler"; | ||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; | ||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; | ||
import { useEventEmitter } from "../../../hooks/useEventEmitter"; | ||
import { UPDATE_EVENT } from "../../../stores/AsyncStore"; | ||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; | ||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; | ||
|
||
/** | ||
* The props for the {@link PinnedMessageBanner} component. | ||
*/ | ||
interface PinnedMessageBannerProps { | ||
/** | ||
* The permalink creator to use. | ||
*/ | ||
permalinkCreator: RoomPermalinkCreator; | ||
/** | ||
* The room where the banner is displayed | ||
*/ | ||
room: Room; | ||
} | ||
|
||
/** | ||
* A banner that displays the pinned messages in a room. | ||
*/ | ||
export function PinnedMessageBanner({ room }: PinnedMessageBannerProps): JSX.Element | null { | ||
const pinnedEventIds = usePinnedEvents(room); | ||
const eventCount = pinnedEventIds.length; | ||
const isSinglePinnedEvent = eventCount === 1; | ||
|
||
const [currentEventIndex, setCurrentEventIndex] = useState(eventCount - 1); | ||
// If the list of pinned events changes, we need to make sure the current index isn't out of bound | ||
useEffect(() => { | ||
setCurrentEventIndex((currentEventIndex) => Math.min(currentEventIndex, eventCount - 1)); | ||
}, [eventCount]); | ||
|
||
// Fetch the pinned event | ||
const pinnedEvent = useFetchPinnedEvent(room, pinnedEventIds[currentEventIndex]); | ||
// Generate a preview for the pinned event | ||
const eventPreview = useMemo(() => { | ||
if (!pinnedEvent) return null; | ||
return MessagePreviewStore.instance.generatePreviewForEvent(pinnedEvent); | ||
}, [pinnedEvent]); | ||
|
||
if (!pinnedEvent) return null; | ||
|
||
return ( | ||
<div className="mx_PinnedMessageBanner" data-single-message={isSinglePinnedEvent}> | ||
<button | ||
aria-label={_t("room|pinned_message_banner|go_to_message")} | ||
type="button" | ||
className="mx_PinnedMessageBanner_main" | ||
onClick={() => setCurrentEventIndex((currentEventIndex) => ++currentEventIndex % eventCount)} | ||
> | ||
{!isSinglePinnedEvent && <Indicators count={eventCount} currentIndex={currentEventIndex} />} | ||
<PinIcon width="20" className="mx_PinnedMessageBanner_PinIcon" /> | ||
{!isSinglePinnedEvent && ( | ||
<div className="mx_PinnedMessageBanner_title"> | ||
{_t( | ||
"room|pinned_message_banner|title", | ||
{ | ||
index: currentEventIndex + 1, | ||
length: eventCount, | ||
}, | ||
{ bold: (sub) => <span className="mx_PinnedMessageBanner_title_counter">{sub}</span> }, | ||
)} | ||
</div> | ||
)} | ||
{eventPreview && <span className="mx_PinnedMessageBanner_message">{eventPreview}</span>} | ||
</button> | ||
{!isSinglePinnedEvent && <BannerButton room={room} />} | ||
</div> | ||
); | ||
} | ||
|
||
const MAX_INDICATORS = 3; | ||
|
||
/** | ||
* The props for the {@link IndicatorsProps} component. | ||
*/ | ||
interface IndicatorsProps { | ||
/** | ||
* The number of messages pinned | ||
*/ | ||
count: number; | ||
/** | ||
* The current index of the pinned message | ||
*/ | ||
currentIndex: number; | ||
} | ||
|
||
/** | ||
* A component that displays vertical indicators for the pinned messages. | ||
*/ | ||
function Indicators({ count, currentIndex }: IndicatorsProps): JSX.Element { | ||
// We only display a maximum of 3 indicators at one time. | ||
// When there is more than 3 messages pinned, we will cycle through the indicators | ||
|
||
// If there is only 2 messages pinned, we will display 2 indicators | ||
// In case of 1 message pinned, the indicators are not displayed, see {@link PinnedMessageBanner} logic. | ||
const numberOfIndicators = Math.min(count, MAX_INDICATORS); | ||
// The index of the active indicator | ||
const index = currentIndex % numberOfIndicators; | ||
|
||
// We hide the indicators when we are on the last cycle and there are less than 3 remaining messages pinned | ||
const numberOfCycles = Math.ceil(count / numberOfIndicators); | ||
// If the current index is greater than the last cycle index, we are on the last cycle | ||
const isLastCycle = currentIndex >= (numberOfCycles - 1) * MAX_INDICATORS; | ||
// The index of the last message in the last cycle | ||
const lastCycleIndex = numberOfIndicators - (numberOfCycles * numberOfIndicators - count); | ||
|
||
return ( | ||
<div className="mx_PinnedMessageBanner_Indicators"> | ||
{Array.from({ length: numberOfIndicators }).map((_, i) => ( | ||
<Indicator key={i} active={i === index} hided={isLastCycle && lastCycleIndex <= i} /> | ||
))} | ||
</div> | ||
); | ||
} | ||
|
||
/** | ||
* The props for the {@link Indicator} component. | ||
*/ | ||
interface IndicatorProps { | ||
/** | ||
* Whether the indicator is active | ||
*/ | ||
active: boolean; | ||
/** | ||
* Whether the indicator is hided | ||
*/ | ||
hided: boolean; | ||
} | ||
|
||
/** | ||
* A component that displays a vertical indicator for a pinned message. | ||
*/ | ||
function Indicator({ active, hided }: IndicatorProps): JSX.Element { | ||
return ( | ||
<div | ||
className={classNames("mx_PinnedMessageBanner_Indicator", { | ||
"mx_PinnedMessageBanner_Indicator--active": active, | ||
"mx_PinnedMessageBanner_Indicator--hided": hided, | ||
})} | ||
/> | ||
); | ||
} | ||
|
||
function getRightPanelPhase(roomId: string): RightPanelPhases | null { | ||
if (!RightPanelStore.instance.isOpenForRoom(roomId)) return null; | ||
return RightPanelStore.instance.currentCard.phase; | ||
} | ||
|
||
/** | ||
* The props for the {@link BannerButton} component. | ||
*/ | ||
interface BannerButtonProps { | ||
/** | ||
* The room where the banner is displayed | ||
*/ | ||
room: Room; | ||
} | ||
|
||
/** | ||
* A button that allows the user to view or close the list of pinned messages. | ||
*/ | ||
function BannerButton({ room }: BannerButtonProps): JSX.Element { | ||
console.log("BannerButton render"); | ||
|
||
const [currentPhase, setCurrentPhase] = useState<RightPanelPhases | null>(getRightPanelPhase(room.roomId)); | ||
useEventEmitter(RightPanelStore.instance, UPDATE_EVENT, () => setCurrentPhase(getRightPanelPhase(room.roomId))); | ||
const isPinnedMessagesPhase = currentPhase === RightPanelPhases.PinnedMessages; | ||
|
||
return ( | ||
<Button | ||
className="mx_PinnedMessageBanner_actions" | ||
kind="tertiary" | ||
onClick={() => { | ||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.PinnedMessages); | ||
}} | ||
> | ||
{isPinnedMessagesPhase | ||
? _t("room|pinned_message_banner|button_close_list") | ||
: _t("room|pinned_message_banner|button_view_all")} | ||
</Button> | ||
); | ||
} |
Oops, something went wrong.