forked from Mirrors/gomuks
web/timeline: add colors for user displaynames
This commit is contained in:
parent
7e793ec0ba
commit
9b73e755e8
5 changed files with 42 additions and 20 deletions
|
@ -28,12 +28,18 @@ export const getEncryptedMediaURL = (mxc?: string): string | undefined => {
|
||||||
return getMediaURL(mxc, true)
|
return getMediaURL(mxc, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This should be synced with the sender colors in ui/timeline/TimelineEvent.css
|
||||||
const fallbackColors = [
|
const fallbackColors = [
|
||||||
"#a4041d", "#9b2200", "#803f00", "#005f00",
|
"#a4041d", "#9b2200", "#803f00", "#005f00",
|
||||||
"#005c45", "#00548c", "#064ab1", "#5d26cd",
|
"#005c45", "#00548c", "#064ab1", "#5d26cd",
|
||||||
"#822198", "#9f0850",
|
"#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
|
// note: this should stay in sync with fallbackAvatarTemplate in cmd/gomuks.media.go
|
||||||
function makeFallbackAvatar(backgroundColor: string, fallbackCharacter: string): string {
|
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">
|
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 => {
|
export const getAvatarURL = (userID: UserID, content?: Partial<MemberEventContent>): string | undefined => {
|
||||||
const fallbackCharacter = (content?.displayname?.[0]?.toUpperCase() ?? userID[1].toUpperCase()).toWellFormed()
|
const fallbackCharacter = (content?.displayname?.[0]?.toUpperCase() ?? userID[1].toUpperCase()).toWellFormed()
|
||||||
const charCodeSum = userID.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0)
|
const backgroundColor = getUserColor(userID)
|
||||||
const backgroundColor = fallbackColors[charCodeSum % fallbackColors.length]
|
|
||||||
const [server, mediaID] = parseMXC(content?.avatar_url)
|
const [server, mediaID] = parseMXC(content?.avatar_url)
|
||||||
if (!mediaID) {
|
if (!mediaID) {
|
||||||
return makeFallbackAvatar(backgroundColor, fallbackCharacter)
|
return makeFallbackAvatar(backgroundColor, fallbackCharacter)
|
||||||
|
|
|
@ -49,11 +49,6 @@ blockquote.reply-body {
|
||||||
margin-right: .25rem;
|
margin-right: .25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
> span.event-sender {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
> button.close-reply {
|
> button.close-reply {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { use } from "react"
|
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 { RoomStateStore, useRoomEvent, useRoomState } from "@/api/statestore"
|
||||||
import type { EventID, MemDBEvent, MemberEventContent } from "@/api/types"
|
import type { EventID, MemDBEvent, MemberEventContent } from "@/api/types"
|
||||||
import ClientContext from "../ClientContext.ts"
|
import ClientContext from "../ClientContext.ts"
|
||||||
|
@ -89,7 +89,9 @@ export const ReplyBody = ({ room, event, onClose, isThread, isEditing }: ReplyBo
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>}
|
{onClose && <button className="close-reply" onClick={onClose}><CloseButton/></button>}
|
||||||
</div>
|
</div>
|
||||||
<ContentErrorBoundary>
|
<ContentErrorBoundary>
|
||||||
|
|
|
@ -51,8 +51,6 @@ div.timeline-event {
|
||||||
|
|
||||||
> span.event-sender {
|
> span.event-sender {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> span.event-time, > span.event-edited {
|
> 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 {
|
div.event-content > div.event-reactions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import React, { use, useState } from "react"
|
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 { useRoomState } from "@/api/statestore"
|
||||||
import { MemDBEvent, MemberEventContent, UnreadType } from "@/api/types"
|
import { MemDBEvent, MemberEventContent, UnreadType } from "@/api/types"
|
||||||
import { isEventID } from "@/util/validation.ts"
|
import { isEventID } from "@/util/validation.ts"
|
||||||
|
@ -86,14 +86,18 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu }: TimelineEventProps) => {
|
||||||
wrapperClassNames.push("highlight")
|
wrapperClassNames.push("highlight")
|
||||||
}
|
}
|
||||||
let smallAvatar = false
|
let smallAvatar = false
|
||||||
|
let renderAvatar = true
|
||||||
|
let eventTimeOnly = false
|
||||||
if (isSmallEvent(BodyType)) {
|
if (isSmallEvent(BodyType)) {
|
||||||
wrapperClassNames.push("hidden-event")
|
wrapperClassNames.push("hidden-event")
|
||||||
smallAvatar = true
|
smallAvatar = true
|
||||||
|
eventTimeOnly = true
|
||||||
} else if (prevEvt?.sender === evt.sender &&
|
} else if (prevEvt?.sender === evt.sender &&
|
||||||
prevEvt.timestamp + 15 * 60 * 1000 > evt.timestamp &&
|
prevEvt.timestamp + 15 * 60 * 1000 > evt.timestamp &&
|
||||||
!isSmallEvent(getBodyType(prevEvt))) {
|
!isSmallEvent(getBodyType(prevEvt))) {
|
||||||
wrapperClassNames.push("same-sender")
|
wrapperClassNames.push("same-sender")
|
||||||
smallAvatar = true
|
eventTimeOnly = true
|
||||||
|
renderAvatar = false
|
||||||
}
|
}
|
||||||
const fullTime = fullTimeFormatter.format(eventTS)
|
const fullTime = fullTimeFormatter.format(eventTS)
|
||||||
const shortTime = formatShortTime(eventTS)
|
const shortTime = formatShortTime(eventTS)
|
||||||
|
@ -104,7 +108,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu }: TimelineEventProps) => {
|
||||||
{!disableMenu && <div className={`context-menu-container ${forceContextMenuOpen ? "force-open" : ""}`}>
|
{!disableMenu && <div className={`context-menu-container ${forceContextMenuOpen ? "force-open" : ""}`}>
|
||||||
<EventMenu evt={evt} setForceOpen={setForceContextMenuOpen}/>
|
<EventMenu evt={evt} setForceOpen={setForceContextMenuOpen}/>
|
||||||
</div>}
|
</div>}
|
||||||
<div className="sender-avatar" title={evt.sender}>
|
{renderAvatar && <div className="sender-avatar" title={evt.sender}>
|
||||||
<img
|
<img
|
||||||
className={`${smallAvatar ? "small" : ""} avatar`}
|
className={`${smallAvatar ? "small" : ""} avatar`}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
@ -112,17 +116,18 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu }: TimelineEventProps) => {
|
||||||
onClick={use(LightboxContext)!}
|
onClick={use(LightboxContext)!}
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>}
|
||||||
<div className="event-sender-and-time">
|
{!eventTimeOnly ? <div className="event-sender-and-time">
|
||||||
<span className="event-sender">{memberEvtContent?.displayname || evt.sender}</span>
|
<span className={`event-sender sender-color-${getUserColorIndex(evt.sender)}`}>
|
||||||
|
{memberEvtContent?.displayname || evt.sender}
|
||||||
|
</span>
|
||||||
<span className="event-time" title={fullTime}>{shortTime}</span>
|
<span className="event-time" title={fullTime}>{shortTime}</span>
|
||||||
{(editEventTS && editTime) ? <span className="event-edited" title={editTime}>
|
{(editEventTS && editTime) ? <span className="event-edited" title={editTime}>
|
||||||
(edited at {formatShortTime(editEventTS)})
|
(edited at {formatShortTime(editEventTS)})
|
||||||
</span> : null}
|
</span> : null}
|
||||||
</div>
|
</div> : <div className="event-time-only">
|
||||||
<div className="event-time-only">
|
|
||||||
<span className="event-time" title={editTime ? `${fullTime} - ${editTime}` : fullTime}>{shortTime}</span>
|
<span className="event-time" title={editTime ? `${fullTime} - ${editTime}` : fullTime}>{shortTime}</span>
|
||||||
</div>
|
</div>}
|
||||||
<div className="event-content">
|
<div className="event-content">
|
||||||
{isEventID(replyTo) && BodyType !== HiddenEvent ? <ReplyIDBody
|
{isEventID(replyTo) && BodyType !== HiddenEvent ? <ReplyIDBody
|
||||||
room={roomCtx.store}
|
room={roomCtx.store}
|
||||||
|
|
Loading…
Add table
Reference in a new issue