diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 96c285bc0a6c320f117d39ecb06768719da0dddb..9227f07a69d2e36ee2f5e17bc79077695d110979 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -249,6 +249,7 @@ @import "./views/messages/_MessageActionBar.pcss"; @import "./views/messages/_MessageTimestamp.pcss"; @import "./views/messages/_MjolnirBody.pcss"; +@import "./views/messages/_PinnedSeparator.pcss"; @import "./views/messages/_ReactionsRow.pcss"; @import "./views/messages/_ReactionsRowButton.pcss"; @import "./views/messages/_RedactedBody.pcss"; diff --git a/res/css/views/messages/_PinnedSeparator.pcss b/res/css/views/messages/_PinnedSeparator.pcss new file mode 100644 index 0000000000000000000000000000000000000000..cf1c9495cf45550e36bded592eb764d0a7c2fed1 --- /dev/null +++ b/res/css/views/messages/_PinnedSeparator.pcss @@ -0,0 +1,24 @@ +/* + * 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_PinnedSeparator { + text-align: end; + border-bottom: 1px solid var(--cpd-color-icon-accent-primary); + font: var(--cpd-font-body-sm-semibold); + color: var(--cpd-color-text-action-accent); + text-transform: uppercase; + margin-bottom: var(--cpd-space-1x); +} diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 07b600484ac93a0e794421a4cd38f09a17bd84ef..9b704d1574d6372ec0e0118faeacf7fe75151a41 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -56,6 +56,8 @@ import { MainGrouper } from "./grouper/MainGrouper"; import { CreationGrouper } from "./grouper/CreationGrouper"; import { _t } from "../../languageHandler"; import { getLateEventInfo } from "./grouper/LateEventGrouper"; +import PinningUtils from "../../utils/PinningUtils"; +import { PinnedSeparator } from "../views/messages/PinnedSeparator"; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = [EventType.Sticker, EventType.RoomMessage]; @@ -767,6 +769,15 @@ export default class MessagePanel extends React.Component<IProps, IState> { } const cli = MatrixClientPeg.safeGet(); + + if (SettingsStore.getValue<boolean>("feature_pinning") && PinningUtils.isPinned(cli, mxEv)) { + ret.push( + <li key={`${ts1}-pinned`}> + <PinnedSeparator /> + </li>, + ); + } + let lastInSection = true; if (nextEventWithTile) { const nextEv = nextEventWithTile; diff --git a/src/components/views/messages/PinnedSeparator.tsx b/src/components/views/messages/PinnedSeparator.tsx new file mode 100644 index 0000000000000000000000000000000000000000..371a8db1957ff4698a37777b07ae79431ab6664e --- /dev/null +++ b/src/components/views/messages/PinnedSeparator.tsx @@ -0,0 +1,30 @@ +/* + * 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 } from "react"; + +import { _t } from "../../../languageHandler"; + +/** + * A separator to be displayed between pinned messages and the rest of the timeline. + */ +export function PinnedSeparator(): JSX.Element { + return ( + <div className="mx_PinnedSeparator" role="separator" aria-label={_t("timeline|pinned_separator_description")}> + {_t("timeline|pinned_separator")} + </div> + ); +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1b7b1f2ed99122d491e51f6dc34b9ad3caeea8f5..7c6dde7b0be245c8057a2fc6316dff8a94d9bc49 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3523,6 +3523,8 @@ "no_permission_messages_before_join": "You don't have permission to view messages from before you joined.", "pending_moderation": "Message pending moderation", "pending_moderation_reason": "Message pending moderation: %(reason)s", + "pinned_separator": "pinned", + "pinned_separator_description": "The following message is pinned.", "reactions": { "add_reaction_prompt": "Add reaction", "custom_reaction_fallback_label": "Custom reaction", diff --git a/src/utils/PinningUtils.ts b/src/utils/PinningUtils.ts index f9750a3ed2280081f3c49e9f10d0be8aa275c33a..16ec4da45e9d8a2c46a83bf0006d6008bbd758f0 100644 --- a/src/utils/PinningUtils.ts +++ b/src/utils/PinningUtils.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixEvent, EventType, M_POLL_START } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, EventType, M_POLL_START, MatrixClient, EventTimeline } from "matrix-js-sdk/src/matrix"; export default class PinningUtils { /** @@ -38,4 +38,22 @@ export default class PinningUtils { return true; } + + /** + * Determines if the given event is pinned. + * @param matrixClient + * @param mxEvent + */ + public static isPinned(matrixClient: MatrixClient, mxEvent: MatrixEvent): boolean { + const room = matrixClient.getRoom(mxEvent.getRoomId()); + if (!room) return false; + + const pinnedEvent = room + .getLiveTimeline() + .getState(EventTimeline.FORWARDS) + ?.getStateEvents(EventType.RoomPinnedEvents, ""); + if (!pinnedEvent) return false; + const content = pinnedEvent.getContent(); + return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(mxEvent.getId()); + } }