Skip to content

Commit

Permalink
web/timeline: render MSC4144 per-message profiles
Browse files Browse the repository at this point in the history
Signed-off-by: Sumner Evans <[email protected]>
  • Loading branch information
sumnerevans committed Dec 25, 2024
1 parent 5fbb8a2 commit 431ce9c
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 15 deletions.
5 changes: 5 additions & 0 deletions web/src/api/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ 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)
if (content?.avatar_file) {
const [server, mediaID] = parseMXC(content.avatar_file.url)
const fallback = `${backgroundColor}:${fallbackCharacter}`
return `_gomuks/media/${server}/${mediaID}?encrypted=true&fallback=${encodeURIComponent(fallback)}`
}
const [server, mediaID] = parseMXC(content?.avatar_url)
if (!mediaID) {
return makeFallbackAvatar(backgroundColor, fallbackCharacter)
Expand Down
6 changes: 6 additions & 0 deletions web/src/api/types/mxtypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export interface EncryptedEventContent {
export interface UserProfile {
displayname?: string
avatar_url?: ContentURI
avatar_file?: EncryptedFile
[custom: string]: unknown
}

Expand Down Expand Up @@ -153,6 +154,10 @@ export interface URLPreview {
"og:description"?: string
}

export interface BeeperPerMessageProfile extends UserProfile {
id: string
}

export interface BaseMessageEventContent {
msgtype: string
body: string
Expand All @@ -165,6 +170,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 {
Expand Down
10 changes: 10 additions & 0 deletions web/src/ui/timeline/ReplyBody.css
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ blockquote.reply-body {
color: var(--secondary-text-color);
}

> div.reply-sender > div.per-message-event-sender {
color: var(--secondary-text-color);
font-size: 0.85rem;
margin: 0.3rem;

> span.via {
margin-right: 0.3rem;
}
}

> div.reply-sender {
display: flex;
align-items: center;
Expand Down
22 changes: 16 additions & 6 deletions web/src/ui/timeline/ReplyBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -57,7 +57,7 @@ export const ReplyIDBody = ({ room, eventID, isThread, small }: ReplyIDBodyProps
Reply to unknown event<br/><code>{eventID}</code>
</blockquote>
}
return <ReplyBody room={room} event={event} isThread={isThread} small={small}/>
return <ReplyBody room={room} event={event} isThread={isThread} small={small} />
}

const onClickReply = (evt: React.MouseEvent) => {
Expand Down Expand Up @@ -99,22 +99,32 @@ export const ReplyBody = ({
if (small) {
classNames.push("small")
}
const userColorIndex = getUserColorIndex(event.sender)
const perMessageSender = getPerMessageProfile(event)
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, perMessageSender ?? memberEvtContent)}
alt=""
/>
</div>
<span className={`event-sender sender-color-${userColorIndex}`}>
{getDisplayname(event.sender, memberEvtContent)}
{getDisplayname(event.sender, perMessageSender ?? memberEvtContent)}
</span>
{perMessageSender && <div className="per-message-event-sender">
<span className="via">via</span>
<span className={`event-sender sender-color-${getUserColorIndex(event.sender)}`}>
{getDisplayname(event.sender, memberEvtContent)}
</span>
</div>}
{onClose && <div className="buttons">
{onSetSilent && (isExplicitInThread || !isThread) && <TooltipButton
tooltipText={isSilent
Expand Down
15 changes: 15 additions & 0 deletions web/src/ui/timeline/TimelineEvent.css
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ div.timeline-event {
cursor: var(--clickable-cursor);
}

> div.per-message-event-sender {
color: var(--secondary-text-color);
font-size: 0.85rem;

> span.via {
margin-right: 0.3rem;
}

> 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);
Expand Down
39 changes: 31 additions & 8 deletions web/src/ui/timeline/TimelineEvent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,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 { EventFullMenu, EventHoverMenu, getModalStyleFromMouse } from "./menu"
import ErrorIcon from "@/icons/error.svg?react"
import PendingIcon from "@/icons/pending.svg?react"
Expand Down Expand Up @@ -100,6 +100,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven
}
const memberEvt = useRoomMember(client, roomCtx.store, evt.sender)
const memberEvtContent = memberEvt?.content as MemberEventContent | undefined
let renderMemberEvtContent = memberEvtContent
const BodyType = getBodyType(evt)
const eventTS = new Date(evt.timestamp)
const editEventTS = evt.last_edit ? new Date(evt.last_edit.timestamp) : null
Expand Down Expand Up @@ -150,6 +151,17 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven
replyInMessage = replyElem
}
}
const perMessageSender = getPerMessageProfile(evt)
const prevPerMessageSender = getPerMessageProfile(prevEvt)
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
Expand All @@ -163,6 +175,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven
&& dateSeparator === null
&& !replyAboveMessage
&& !isSmallEvent(getBodyType(prevEvt))
&& prevPerMessageSender?.id === perMessageSender?.id
) {
wrapperClassNames.push("same-sender")
eventTimeOnly = true
Expand All @@ -184,26 +197,36 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven
{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}
>
<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)}`}
data-target-user={evt.sender}
onClick={roomCtx.appendMentionToComposer}
className={`event-sender sender-color-${getUserColorIndex(perMessageSender?.id ?? evt.sender)}`}
data-target-user={perMessageSender ? undefined : evt.sender}
onClick={perMessageSender ? undefined : roomCtx.appendMentionToComposer}
>
{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}
>
{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)})
Expand All @@ -220,7 +243,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven
{evt.reactions ? <EventReactions reactions={evt.reactions}/> : null}
</div>
{!evt.event_id.startsWith("~") && roomCtx.store.preferences.display_read_receipts &&
<ReadReceipts room={roomCtx.store} eventID={evt.event_id} />}
<ReadReceipts room={roomCtx.store} eventID={evt.event_id}/>}
{evt.sender === client.userID && evt.transaction_id ? <EventSendStatus evt={evt}/> : null}
</div>
return <>
Expand Down
7 changes: 6 additions & 1 deletion web/src/ui/timeline/content/index.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -104,3 +104,8 @@ 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"]
}

0 comments on commit 431ce9c

Please sign in to comment.