forked from Mirrors/gomuks
web/timeline: render MSC4144 per-message profiles (#566)
Signed-off-by: Sumner Evans <me@sumnerevans.com>
This commit is contained in:
parent
bdc823742e
commit
5b5df65f39
7 changed files with 104 additions and 15 deletions
|
@ -81,12 +81,13 @@ function getFallbackCharacter(from: unknown, idx: number): string {
|
|||
export const getAvatarURL = (userID: UserID, content?: UserProfile | null): string | undefined => {
|
||||
const fallbackCharacter = getFallbackCharacter(content?.displayname, 0) || getFallbackCharacter(userID, 1)
|
||||
const backgroundColor = getUserColor(userID)
|
||||
const [server, mediaID] = parseMXC(content?.avatar_url)
|
||||
const [server, mediaID] = parseMXC(content?.avatar_file?.url ?? content?.avatar_url)
|
||||
if (!mediaID) {
|
||||
return makeFallbackAvatar(backgroundColor, fallbackCharacter)
|
||||
}
|
||||
const encrypted = !!content?.avatar_file
|
||||
const fallback = `${backgroundColor}:${fallbackCharacter}`
|
||||
return `_gomuks/media/${server}/${mediaID}?encrypted=false&fallback=${encodeURIComponent(fallback)}`
|
||||
return `_gomuks/media/${server}/${mediaID}?encrypted=${encrypted}&fallback=${encodeURIComponent(fallback)}`
|
||||
}
|
||||
|
||||
interface RoomForAvatarURL {
|
||||
|
|
|
@ -73,6 +73,7 @@ export interface EncryptedEventContent {
|
|||
export interface UserProfile {
|
||||
displayname?: string
|
||||
avatar_url?: ContentURI
|
||||
avatar_file?: EncryptedFile
|
||||
[custom: string]: unknown
|
||||
}
|
||||
|
||||
|
@ -177,6 +178,10 @@ export interface URLPreview {
|
|||
"og:description"?: string
|
||||
}
|
||||
|
||||
export interface BeeperPerMessageProfile extends UserProfile {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface BaseMessageEventContent {
|
||||
msgtype: string
|
||||
body: string
|
||||
|
@ -189,6 +194,7 @@ export interface BaseMessageEventContent {
|
|||
"page.codeberg.everypizza.msc4193.spoiler.reason"?: string
|
||||
"m.url_previews"?: URLPreview[]
|
||||
"com.beeper.linkpreviews"?: URLPreview[]
|
||||
"com.beeper.per_message_profile"?: BeeperPerMessageProfile
|
||||
}
|
||||
|
||||
export interface TextMessageEventContent extends BaseMessageEventContent {
|
||||
|
|
|
@ -103,6 +103,16 @@ blockquote.reply-body {
|
|||
}
|
||||
}
|
||||
|
||||
> div.per-message-event-sender {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: .75rem;
|
||||
margin: 0 .25rem;
|
||||
|
||||
> span.via {
|
||||
margin-right: .25rem;
|
||||
}
|
||||
}
|
||||
|
||||
> div.buttons {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
|
|
|
@ -20,7 +20,7 @@ import type { EventID, MemDBEvent, MemberEventContent } from "@/api/types"
|
|||
import { getDisplayname } from "@/util/validation.ts"
|
||||
import ClientContext from "../ClientContext.ts"
|
||||
import TooltipButton from "../util/TooltipButton.tsx"
|
||||
import { ContentErrorBoundary, getBodyType } from "./content"
|
||||
import { ContentErrorBoundary, getBodyType, getPerMessageProfile } from "./content"
|
||||
import CloseIcon from "@/icons/close.svg?react"
|
||||
import NotificationsOffIcon from "@/icons/notifications-off.svg?react"
|
||||
import NotificationsIcon from "@/icons/notifications.svg?react"
|
||||
|
@ -102,22 +102,47 @@ export const ReplyBody = ({
|
|||
if (small) {
|
||||
classNames.push("small")
|
||||
}
|
||||
const userColorIndex = getUserColorIndex(event.sender)
|
||||
const perMessageSender = getPerMessageProfile(event)
|
||||
let renderMemberEvtContent = memberEvtContent
|
||||
if (perMessageSender) {
|
||||
renderMemberEvtContent = {
|
||||
membership: "join",
|
||||
displayname: perMessageSender.displayname ?? memberEvtContent?.displayname,
|
||||
avatar_url: perMessageSender.avatar_url ?? memberEvtContent?.avatar_url,
|
||||
avatar_file: perMessageSender.avatar_file ?? memberEvtContent?.avatar_file,
|
||||
}
|
||||
}
|
||||
const userColorIndex = getUserColorIndex(perMessageSender?.id ?? event.sender)
|
||||
classNames.push(`sender-color-${userColorIndex}`)
|
||||
return <blockquote data-reply-to={event.event_id} className={classNames.join(" ")} onClick={onClickReply}>
|
||||
{small && <div className="reply-spine"/>}
|
||||
<div className="reply-sender">
|
||||
<div className="sender-avatar" title={event.sender}>
|
||||
<div
|
||||
className="sender-avatar"
|
||||
title={perMessageSender ? `${perMessageSender.id} via ${event.sender}` : event.sender}
|
||||
>
|
||||
<img
|
||||
className="small avatar"
|
||||
loading="lazy"
|
||||
src={getAvatarURL(event.sender, memberEvtContent)}
|
||||
src={getAvatarURL(perMessageSender?.id ?? event.sender, renderMemberEvtContent)}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<span className={`event-sender sender-color-${userColorIndex}`}>
|
||||
{getDisplayname(event.sender, memberEvtContent)}
|
||||
<span
|
||||
className={`event-sender sender-color-${userColorIndex}`}
|
||||
title={perMessageSender ? perMessageSender.id : event.sender}
|
||||
>
|
||||
{getDisplayname(event.sender, renderMemberEvtContent)}
|
||||
</span>
|
||||
{perMessageSender && <div className="per-message-event-sender">
|
||||
<span className="via">via</span>
|
||||
<span
|
||||
className={`event-sender sender-color-${getUserColorIndex(event.sender)}`}
|
||||
title={event.sender}
|
||||
>
|
||||
{getDisplayname(event.sender, memberEvtContent)}
|
||||
</span>
|
||||
</div>}
|
||||
{onClose && <div className="buttons">
|
||||
{onSetSilent && (isExplicitInThread || !isThread) && <TooltipButton
|
||||
tooltipText={isSilent
|
||||
|
|
|
@ -64,6 +64,21 @@ div.timeline-event {
|
|||
cursor: var(--clickable-cursor);
|
||||
}
|
||||
|
||||
> div.per-message-event-sender {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: .75rem;
|
||||
|
||||
> span.via {
|
||||
margin-right: .25rem;
|
||||
}
|
||||
|
||||
> span.event-sender {
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
cursor: var(--clickable-cursor);
|
||||
}
|
||||
}
|
||||
|
||||
> span.event-time, > span.event-edited {
|
||||
font-size: .7rem;
|
||||
color: var(--secondary-text-color);
|
||||
|
|
|
@ -27,7 +27,7 @@ import { useRoomContext } from "../roomview/roomcontext.ts"
|
|||
import ReadReceipts from "./ReadReceipts.tsx"
|
||||
import { ReplyIDBody } from "./ReplyBody.tsx"
|
||||
import URLPreviews from "./URLPreviews.tsx"
|
||||
import { ContentErrorBoundary, HiddenEvent, getBodyType, isSmallEvent } from "./content"
|
||||
import { ContentErrorBoundary, HiddenEvent, getBodyType, getPerMessageProfile, isSmallEvent } from "./content"
|
||||
import { EventFixedMenu, EventFullMenu, EventHoverMenu, getModalStyleFromMouse } from "./menu"
|
||||
import ErrorIcon from "@/icons/error.svg?react"
|
||||
import PendingIcon from "@/icons/pending.svg?react"
|
||||
|
@ -170,6 +170,18 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies, isFocused }: T
|
|||
replyInMessage = replyElem
|
||||
}
|
||||
}
|
||||
const perMessageSender = getPerMessageProfile(evt)
|
||||
const prevPerMessageSender = getPerMessageProfile(prevEvt)
|
||||
let renderMemberEvtContent = memberEvtContent
|
||||
if (perMessageSender) {
|
||||
renderMemberEvtContent = {
|
||||
membership: "join",
|
||||
displayname: perMessageSender.displayname ?? memberEvtContent?.displayname,
|
||||
avatar_url: perMessageSender.avatar_url ?? memberEvtContent?.avatar_url,
|
||||
avatar_file: perMessageSender.avatar_file ?? memberEvtContent?.avatar_file,
|
||||
}
|
||||
}
|
||||
|
||||
let smallAvatar = false
|
||||
let renderAvatar = true
|
||||
let eventTimeOnly = false
|
||||
|
@ -183,6 +195,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies, isFocused }: T
|
|||
&& dateSeparator === null
|
||||
&& !replyAboveMessage
|
||||
&& !isSmallEvent(getBodyType(prevEvt))
|
||||
&& prevPerMessageSender?.id === perMessageSender?.id
|
||||
) {
|
||||
wrapperClassNames.push("same-sender")
|
||||
eventTimeOnly = true
|
||||
|
@ -209,7 +222,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies, isFocused }: T
|
|||
{replyAboveMessage}
|
||||
{renderAvatar && <div
|
||||
className="sender-avatar"
|
||||
title={evt.sender}
|
||||
title={perMessageSender ? `${perMessageSender.id} via ${evt.sender}` : evt.sender}
|
||||
data-target-panel="user"
|
||||
data-target-user={evt.sender}
|
||||
onClick={mainScreen.clickRightPanelOpener}
|
||||
|
@ -217,18 +230,30 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies, isFocused }: T
|
|||
<img
|
||||
className={`${smallAvatar ? "small" : ""} avatar`}
|
||||
loading="lazy"
|
||||
src={getAvatarURL(evt.sender, memberEvtContent)}
|
||||
src={getAvatarURL(perMessageSender?.id ?? evt.sender, renderMemberEvtContent)}
|
||||
alt=""
|
||||
/>
|
||||
</div>}
|
||||
{!eventTimeOnly ? <div className="event-sender-and-time">
|
||||
<span
|
||||
className={`event-sender sender-color-${getUserColorIndex(evt.sender)}`}
|
||||
className={`event-sender sender-color-${getUserColorIndex(perMessageSender?.id ?? evt.sender)}`}
|
||||
data-target-user={evt.sender}
|
||||
onClick={roomCtx.appendMentionToComposer}
|
||||
onClick={perMessageSender ? undefined : roomCtx.appendMentionToComposer}
|
||||
title={perMessageSender ? perMessageSender.id : evt.sender}
|
||||
>
|
||||
{getDisplayname(evt.sender, memberEvtContent)}
|
||||
{getDisplayname(evt.sender, renderMemberEvtContent)}
|
||||
</span>
|
||||
{perMessageSender && <div className="per-message-event-sender">
|
||||
<span className="via">via</span>
|
||||
<span
|
||||
className={`event-sender sender-color-${getUserColorIndex(evt.sender)}`}
|
||||
data-target-user={evt.sender}
|
||||
onClick={roomCtx.appendMentionToComposer}
|
||||
title={evt.sender}
|
||||
>
|
||||
{getDisplayname(evt.sender, memberEvtContent)}
|
||||
</span>
|
||||
</div>}
|
||||
<span className="event-time" title={fullTime}>{shortTime}</span>
|
||||
{(editEventTS && editTime) ? <span className="event-edited" title={editTime}>
|
||||
(edited at {formatShortTime(editEventTS)})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react"
|
||||
import { MemDBEvent } from "@/api/types"
|
||||
import { BeeperPerMessageProfile, MemDBEvent, MessageEventContent } from "@/api/types"
|
||||
import ACLBody from "./ACLBody.tsx"
|
||||
import EncryptedBody from "./EncryptedBody.tsx"
|
||||
import HiddenEvent from "./HiddenEvent.tsx"
|
||||
|
@ -113,3 +113,10 @@ export function isSmallEvent(bodyType: React.FunctionComponent<EventContentProps
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function getPerMessageProfile(evt: MemDBEvent | null): BeeperPerMessageProfile | undefined {
|
||||
if (evt === null || evt.type !== "m.room.message" && evt.type !== "m.sticker") {
|
||||
return undefined
|
||||
}
|
||||
return (evt.content as MessageEventContent)["com.beeper.per_message_profile"]
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue