From 9b73e755e84916b1b5e4bfd0ef275d2f442af0c2 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 28 Oct 2024 00:42:43 +0200 Subject: [PATCH] web/timeline: add colors for user displaynames --- web/src/api/media.ts | 9 +++++++-- web/src/ui/timeline/ReplyBody.css | 5 ----- web/src/ui/timeline/ReplyBody.tsx | 6 ++++-- web/src/ui/timeline/TimelineEvent.css | 19 +++++++++++++++++-- web/src/ui/timeline/TimelineEvent.tsx | 23 ++++++++++++++--------- 5 files changed, 42 insertions(+), 20 deletions(-) diff --git a/web/src/api/media.ts b/web/src/api/media.ts index 9822d2a..7f0b53d 100644 --- a/web/src/api/media.ts +++ b/web/src/api/media.ts @@ -28,12 +28,18 @@ export const getEncryptedMediaURL = (mxc?: string): string | undefined => { return getMediaURL(mxc, true) } +// This should be synced with the sender colors in ui/timeline/TimelineEvent.css const fallbackColors = [ "#a4041d", "#9b2200", "#803f00", "#005f00", "#005c45", "#00548c", "#064ab1", "#5d26cd", "#822198", "#9f0850", ] +export const getUserColorIndex = (userID: UserID) => + userID.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0) % fallbackColors.length + +export const getUserColor = (userID: UserID) => fallbackColors[getUserColorIndex(userID)] + // note: this should stay in sync with fallbackAvatarTemplate in cmd/gomuks.media.go function makeFallbackAvatar(backgroundColor: string, fallbackCharacter: string): string { return "data:image/svg+xml," + encodeURIComponent(` @@ -55,8 +61,7 @@ function escapeHTMLChar(char: string): string { export const getAvatarURL = (userID: UserID, content?: Partial): string | undefined => { const fallbackCharacter = (content?.displayname?.[0]?.toUpperCase() ?? userID[1].toUpperCase()).toWellFormed() - const charCodeSum = userID.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0) - const backgroundColor = fallbackColors[charCodeSum % fallbackColors.length] + const backgroundColor = getUserColor(userID) const [server, mediaID] = parseMXC(content?.avatar_url) if (!mediaID) { return makeFallbackAvatar(backgroundColor, fallbackCharacter) diff --git a/web/src/ui/timeline/ReplyBody.css b/web/src/ui/timeline/ReplyBody.css index 3561407..f3c4c84 100644 --- a/web/src/ui/timeline/ReplyBody.css +++ b/web/src/ui/timeline/ReplyBody.css @@ -49,11 +49,6 @@ blockquote.reply-body { margin-right: .25rem; } - > span.event-sender { - overflow: hidden; - text-overflow: ellipsis; - } - > button.close-reply { display: flex; margin-left: auto; diff --git a/web/src/ui/timeline/ReplyBody.tsx b/web/src/ui/timeline/ReplyBody.tsx index 2e920a5..c056420 100644 --- a/web/src/ui/timeline/ReplyBody.tsx +++ b/web/src/ui/timeline/ReplyBody.tsx @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import { use } from "react" -import { getAvatarURL } from "@/api/media.ts" +import { getAvatarURL, getUserColorIndex } from "@/api/media.ts" import { RoomStateStore, useRoomEvent, useRoomState } from "@/api/statestore" import type { EventID, MemDBEvent, MemberEventContent } from "@/api/types" import ClientContext from "../ClientContext.ts" @@ -89,7 +89,9 @@ export const ReplyBody = ({ room, event, onClose, isThread, isEditing }: ReplyBo alt="" /> - {memberEvtContent?.displayname || event.sender} + + {memberEvtContent?.displayname || event.sender} + {onClose && } diff --git a/web/src/ui/timeline/TimelineEvent.css b/web/src/ui/timeline/TimelineEvent.css index e4861d9..eb7d08b 100644 --- a/web/src/ui/timeline/TimelineEvent.css +++ b/web/src/ui/timeline/TimelineEvent.css @@ -51,8 +51,6 @@ div.timeline-event { > span.event-sender { font-weight: bold; - overflow: hidden; - text-overflow: ellipsis; } > span.event-time, > span.event-edited { @@ -148,6 +146,23 @@ div.timeline-event { } } +span.event-sender { + overflow: hidden; + text-overflow: ellipsis; + + /* These should be synced with the avatar colors in api/media.ts */ + &.sender-color-0 { color: #a4041d; } + &.sender-color-1 { color: #9b2200; } + &.sender-color-2 { color: #803f00; } + &.sender-color-3 { color: #005f00; } + &.sender-color-4 { color: #005c45; } + &.sender-color-5 { color: #00548c; } + &.sender-color-6 { color: #064ab1; } + &.sender-color-7 { color: #5d26cd; } + &.sender-color-8 { color: #822198; } + &.sender-color-9 { color: #9f0850; } +} + div.event-content > div.event-reactions { display: flex; flex-wrap: wrap; diff --git a/web/src/ui/timeline/TimelineEvent.tsx b/web/src/ui/timeline/TimelineEvent.tsx index 6d527c8..b30e159 100644 --- a/web/src/ui/timeline/TimelineEvent.tsx +++ b/web/src/ui/timeline/TimelineEvent.tsx @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { use, useState } from "react" -import { getAvatarURL, getMediaURL } from "@/api/media.ts" +import { getAvatarURL, getMediaURL, getUserColorIndex } from "@/api/media.ts" import { useRoomState } from "@/api/statestore" import { MemDBEvent, MemberEventContent, UnreadType } from "@/api/types" import { isEventID } from "@/util/validation.ts" @@ -86,14 +86,18 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu }: TimelineEventProps) => { wrapperClassNames.push("highlight") } let smallAvatar = false + let renderAvatar = true + let eventTimeOnly = false if (isSmallEvent(BodyType)) { wrapperClassNames.push("hidden-event") smallAvatar = true + eventTimeOnly = true } else if (prevEvt?.sender === evt.sender && prevEvt.timestamp + 15 * 60 * 1000 > evt.timestamp && !isSmallEvent(getBodyType(prevEvt))) { wrapperClassNames.push("same-sender") - smallAvatar = true + eventTimeOnly = true + renderAvatar = false } const fullTime = fullTimeFormatter.format(eventTS) const shortTime = formatShortTime(eventTS) @@ -104,7 +108,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu }: TimelineEventProps) => { {!disableMenu &&
} -
+ {renderAvatar &&
{ onClick={use(LightboxContext)!} alt="" /> -
-
- {memberEvtContent?.displayname || evt.sender} +
} + {!eventTimeOnly ?
+ + {memberEvtContent?.displayname || evt.sender} + {shortTime} {(editEventTS && editTime) ? (edited at {formatShortTime(editEventTS)}) : null} -
-
+
:
{shortTime} -
+
}
{isEventID(replyTo) && BodyType !== HiddenEvent ?