web/timeline: add colors for user displaynames

This commit is contained in:
Tulir Asokan 2024-10-28 00:42:43 +02:00
parent 7e793ec0ba
commit 9b73e755e8
5 changed files with 42 additions and 20 deletions

View file

@ -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(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
@ -55,8 +61,7 @@ function escapeHTMLChar(char: string): string {
export const getAvatarURL = (userID: UserID, content?: Partial<MemberEventContent>): 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)

View file

@ -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;

View file

@ -14,7 +14,7 @@
// 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 { 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=""
/>
</div>
<span className="event-sender">{memberEvtContent?.displayname || event.sender}</span>
<span className={`event-sender sender-color-${getUserColorIndex(event.sender)}`}>
{memberEvtContent?.displayname || event.sender}
</span>
{onClose && <button className="close-reply" onClick={onClose}><CloseButton/></button>}
</div>
<ContentErrorBoundary>

View file

@ -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;

View file

@ -14,7 +14,7 @@
// 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, { 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 && <div className={`context-menu-container ${forceContextMenuOpen ? "force-open" : ""}`}>
<EventMenu evt={evt} setForceOpen={setForceContextMenuOpen}/>
</div>}
<div className="sender-avatar" title={evt.sender}>
{renderAvatar && <div className="sender-avatar" title={evt.sender}>
<img
className={`${smallAvatar ? "small" : ""} avatar`}
loading="lazy"
@ -112,17 +116,18 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu }: TimelineEventProps) => {
onClick={use(LightboxContext)!}
alt=""
/>
</div>
<div className="event-sender-and-time">
<span className="event-sender">{memberEvtContent?.displayname || evt.sender}</span>
</div>}
{!eventTimeOnly ? <div className="event-sender-and-time">
<span className={`event-sender sender-color-${getUserColorIndex(evt.sender)}`}>
{memberEvtContent?.displayname || evt.sender}
</span>
<span className="event-time" title={fullTime}>{shortTime}</span>
{(editEventTS && editTime) ? <span className="event-edited" title={editTime}>
(edited at {formatShortTime(editEventTS)})
</span> : null}
</div>
<div className="event-time-only">
</div> : <div className="event-time-only">
<span className="event-time" title={editTime ? `${fullTime} - ${editTime}` : fullTime}>{shortTime}</span>
</div>
</div>}
<div className="event-content">
{isEventID(replyTo) && BodyType !== HiddenEvent ? <ReplyIDBody
room={roomCtx.store}