web/{timeline,composer}: render m.room.tombstone events (#608)
Some checks are pending
Go / Lint Go (old) (push) Waiting to run
Go / Lint Go (latest) (push) Waiting to run
JS / Lint JS (push) Waiting to run

This commit is contained in:
nexy7574 2025-03-28 11:10:17 +00:00 committed by GitHub
parent 5f50cf8e77
commit 769d60c459
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 103 additions and 2 deletions

View file

@ -102,6 +102,11 @@ export interface RoomNameEventContent {
name?: string
}
export interface RoomCanonicalAliasEventContent {
alias?: RoomAlias | null
alt_aliases?: RoomAlias[]
}
export interface RoomTopicEventContent {
topic?: string
}

View file

@ -14,6 +14,11 @@ div.message-composer {
text-wrap: auto !important;
}
&.tombstoned {
min-height: unset;
padding: .5rem;
}
> div.input-area {
display: flex;
align-items: center;

View file

@ -13,9 +13,19 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import React, { CSSProperties, use, useCallback, useEffect, useLayoutEffect, useReducer, useRef, useState } from "react"
import React, {
CSSProperties,
JSX,
use,
useCallback,
useEffect,
useLayoutEffect,
useReducer,
useRef,
useState,
} from "react"
import { ScaleLoader } from "react-spinners"
import { useRoomEvent } from "@/api/statestore"
import { useRoomEvent, useRoomState } from "@/api/statestore"
import type {
EventID,
MediaMessageEventContent,
@ -28,6 +38,7 @@ import type {
import { PartialEmoji, emojiToMarkdown } from "@/util/emoji"
import { isMobileDevice } from "@/util/ismobile.ts"
import { escapeMarkdown } from "@/util/markdown.ts"
import { getServerName } from "@/util/validation.ts"
import ClientContext from "../ClientContext.ts"
import EmojiPicker from "../emojipicker/EmojiPicker.tsx"
import GIFPicker from "../emojipicker/GIFPicker.tsx"
@ -576,6 +587,35 @@ const MessageComposer = () => {
const inlineButtons = state.text === "" || window.innerWidth > 720
const showSendButton = canSend || window.innerWidth > 720
const disableClearMedia = editing && state.media?.msgtype === "m.sticker"
const tombstoneEvent = useRoomState(room, "m.room.tombstone", "")
if (tombstoneEvent !== null) {
const content = tombstoneEvent.content
const hasReplacement = content.replacement_room?.startsWith("!")
let link: JSX.Element | null = null
if (hasReplacement) {
const via = getServerName(tombstoneEvent.sender)
const handleNavigate = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
e.preventDefault()
window.mainScreenContext.setActiveRoom(content.replacement_room, {
via: [via],
})
}
const url = `matrix:roomid/${content.replacement_room.slice(1)}?via=${via}`
link = <a href={url} onClick={handleNavigate}>
Join the new one here
</a>
}
let body = content.body
if (!body) {
body = hasReplacement ? "This room has been replaced." : "This room has been shut down."
}
if (!body.endsWith(".")) {
body += "."
}
return <div className="message-composer tombstoned" ref={composerRef}>
{body} {link}
</div>
}
return <>
{Autocompleter && autocomplete && <div className="autocompletions-wrapper"><Autocompleter
params={autocomplete}

View file

@ -0,0 +1,46 @@
// gomuks - A Matrix client written in Go.
// Copyright (C) 2024 Nexus Nicholson
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { JSX } from "react"
import { TombstoneEventContent } from "@/api/types"
import EventContentProps from "./props.ts"
const RoomTombstoneBody = ({ event, sender }: EventContentProps) => {
const content = event.content as TombstoneEventContent
const end = content.body?.length > 0 ? ` with the message: ${content.body}` : "."
const onClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
e.preventDefault()
window.mainScreenContext.setActiveRoom(content.replacement_room)
}
let description: JSX.Element
if (content.replacement_room?.length && content.replacement_room.startsWith("!")) {
description = (
<span>
replaced this room with&nbsp;
<a onClick={onClick} href={"matrix:roomid/" + content.replacement_room.slice(1)}
className="hicli-matrix-uri">
{content.replacement_room}
</a>{end}
</span>
)
} else {
description = <span>shut down this room{end}</span>
}
return <div className="room-tombstone-body">
{sender?.content.displayname ?? event.sender} {description}
</div>
}
export default RoomTombstoneBody

View file

@ -12,6 +12,7 @@ import PowerLevelBody from "./PowerLevelBody.tsx"
import RedactedBody from "./RedactedBody.tsx"
import RoomAvatarBody from "./RoomAvatarBody.tsx"
import RoomNameBody from "./RoomNameBody.tsx"
import RoomTombstoneBody from "./RoomTombstoneBody.tsx"
import TextMessageBody from "./TextMessageBody.tsx"
import UnknownMessageBody from "./UnknownMessageBody.tsx"
import EventContentProps from "./props.ts"
@ -31,6 +32,7 @@ export { default as RedactedBody } from "./RedactedBody.tsx"
export { default as RoomAvatarBody } from "./RoomAvatarBody.tsx"
export { default as RoomNameBody } from "./RoomNameBody.tsx"
export { default as TextMessageBody } from "./TextMessageBody.tsx"
export { default as RoomTombstoneBody } from "./RoomTombstoneBody.tsx"
export { default as UnknownMessageBody } from "./UnknownMessageBody.tsx"
export type { default as EventContentProps } from "./props.ts"
@ -51,6 +53,8 @@ export function getBodyType(evt: MemDBEvent, forReply = false): React.FunctionCo
return PinnedEventsBody
case "m.room.power_levels":
return PowerLevelBody
case "m.room.tombstone":
return RoomTombstoneBody
}
} else if (evt.state_key !== undefined) {
// State events which must have a non-empty state key
@ -120,6 +124,7 @@ export function isSmallEvent(bodyType: React.FunctionComponent<EventContentProps
case PolicyRuleBody:
case PinnedEventsBody:
case PowerLevelBody:
case RoomTombstoneBody:
return true
default:
return false