Give a space with no icon a colored tile with its initials (#428)

This commit is contained in:
Coracle-Bot 2026-09-07 23:09:02 +00:00 committed by hodlbod
parent 3bd821efc1
commit 7b145c525d
7 changed files with 60 additions and 30 deletions

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames" import cx from "classnames"
import {hash, formatTimestampAsTime} from "@welshman/lib" import {formatTimestampAsTime} from "@welshman/lib"
import type {Maybe} from "@welshman/lib" import type {Maybe} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {Thunks} from "@welshman/app" import {Thunks} from "@welshman/app"
@ -21,7 +21,7 @@
import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte" import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte"
import ChatMessageMenuMobile from "@app/components/ChatMessageMenuMobile.svelte" import ChatMessageMenuMobile from "@app/components/ChatMessageMenuMobile.svelte"
import {app, profiles, user} from "@app/core" import {app, profiles, user} from "@app/core"
import {colors} from "@app/theme" import {colorFor} from "@app/theme"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
interface Props { interface Props {
@ -40,7 +40,7 @@
const profileDisplay = $profiles.display(event.pubkey).$ const profileDisplay = $profiles.display(event.pubkey).$
const thunks = $app.use(Thunks).history const thunks = $app.use(Thunks).history
const thunk = $app.use(Thunks).merge($thunks.filter(t => t.event.id === event.id)) const thunk = $app.use(Thunks).merge($thunks.filter(t => t.event.id === event.id))
const [_, colorValue] = colors[hash(event.pubkey) % colors.length] const colorValue = colorFor(event.pubkey)
const reply = () => replyTo(event) const reply = () => replyTo(event)
const edit = canEdit?.(event) ? () => onEdit?.(event) : undefined const edit = canEdit?.(event) ? () => onEdit?.(event) : undefined

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import Server from "@assets/icons/server.svg?dataurl"
import ImageIcon from "@lib/components/ImageIcon.svelte" import ImageIcon from "@lib/components/ImageIcon.svelte"
import {relays} from "@app/core" import {relays} from "@app/core"
import {relayIconColor, relayIconInitials} from "@app/relays"
type Props = { type Props = {
url: string url: string
@ -12,10 +12,20 @@
const {url, size = 7, ...props}: Props = $props() const {url, size = 7, ...props}: Props = $props()
const relay = $relays.one(url) const relay = $relays.one(url)
const rem = $derived(size * 0.25)
const fallbackStyle = $derived(
`width: ${rem}rem; height: ${rem}rem; min-width: ${rem}rem; min-height: ${rem}rem; font-size: ${rem * 0.4}rem; background-color: ${relayIconColor(url)}; color: white`,
)
</script> </script>
{#if $relay?.icon} {#if $relay?.icon}
<ImageIcon {size} alt="" src={$relay?.icon} class="rounded-full {props.class}" /> <ImageIcon {size} alt="" src={$relay?.icon} class="rounded-full {props.class}" />
{:else} {:else}
<ImageIcon size={size - 2} alt="" src={Server} class={props.class} /> <div
class="flex select-none items-center justify-center rounded-full font-bold {props.class}"
style={fallbackStyle}>
{relayIconInitials(url)}
</div>
{/if} {/if}

View file

@ -26,7 +26,7 @@
<div class="relative"> <div class="relative">
<div <div
class="flex justify-center items-center h-12 w-12 min-w-12 rounded-full border-2 border-solid bg-surface-more border-line"> class="flex justify-center items-center h-12 w-12 min-w-12 rounded-full border-2 border-solid bg-surface-more border-line">
<RelayIcon {url} size={10} class="rounded-full" /> <RelayIcon {url} size={10} />
</div> </div>
</div> </div>
{#if !hideFavorites && $userSpaceUrls.includes(url)} {#if !hideFavorites && $userSpaceUrls.includes(url)}

View file

@ -2,7 +2,6 @@
import cx from "classnames" import cx from "classnames"
import {readable} from "svelte/store" import {readable} from "svelte/store"
import { import {
hash,
gte, gte,
now, now,
uniq, uniq,
@ -40,7 +39,7 @@
import RoomItemContent from "@app/components/RoomItemContent.svelte" import RoomItemContent from "@app/components/RoomItemContent.svelte"
import {profiles, thunks, user} from "@app/core" import {profiles, thunks, user} from "@app/core"
import {noThunks, thunksByEventId} from "@app/thunks" import {noThunks, thunksByEventId} from "@app/thunks"
import {colors} from "@app/theme" import {colorFor} from "@app/theme"
import {ENABLE_ZAPS} from "@app/env" import {ENABLE_ZAPS} from "@app/env"
import type {FeedContext} from "@app/feeds" import type {FeedContext} from "@app/feeds"
import {deriveEvent, deriveEventsForUrl} from "@app/repository" import {deriveEvent, deriveEventsForUrl} from "@app/repository"
@ -71,7 +70,7 @@
const today = formatTimestampAsDate(now()) const today = formatTimestampAsDate(now())
const profileDisplay = $profiles.display(event.pubkey, [url]).$ const profileDisplay = $profiles.display(event.pubkey, [url]).$
const thunk = $derived($thunks.merge($thunksByEventId.get(event.id) ?? noThunks)) const thunk = $derived($thunks.merge($thunksByEventId.get(event.id) ?? noThunks))
const [_, colorValue] = colors[hash(event.pubkey) % colors.length] const colorValue = colorFor(event.pubkey)
const qTag = matchTag(tagSpec("q"), event.tags) const qTag = matchTag(tagSpec("q"), event.tags)
const isQuoteOnly = Boolean( const isQuoteOnly = Boolean(

View file

@ -6,6 +6,7 @@ import {throttled} from "@welshman/store"
import {checkRelayHasLivekit} from "$lib/livekit" import {checkRelayHasLivekit} from "$lib/livekit"
import {app} from "@app/core" import {app} from "@app/core"
import {relaysMostlyRestricted} from "@app/policies" import {relaysMostlyRestricted} from "@app/policies"
import {colorFor} from "@app/theme"
export const encodeRelay = (url: string) => export const encodeRelay = (url: string) =>
encodeURIComponent( encodeURIComponent(
@ -16,6 +17,23 @@ export const encodeRelay = (url: string) =>
export const decodeRelay = (url: string) => normalizeRelayUrl(decodeURIComponent(url)) export const decodeRelay = (url: string) => normalizeRelayUrl(decodeURIComponent(url))
const relayIconHost = (url: string) =>
normalizeRelayUrl(url)
.replace(/^wss?:\/\//, "")
.replace(/[:/].*$/, "")
// Darkened so white initials clear 4.5:1 on every color in the palette.
export const relayIconColor = (url: string) =>
`color-mix(in oklab, ${colorFor(relayIconHost(url))} 80%, black)`
export const relayIconInitials = (url: string) => {
const parts = relayIconHost(url).split(".")
const hasSubdomain = parts.length > 2
const [first = "", second = ""] = parts
return (hasSubdomain ? first.slice(0, 1) + second.slice(0, 1) : first.slice(0, 2)).toUpperCase()
}
export const deriveSocket = (url: string) => { export const deriveSocket = (url: string) => {
const socket = app.get().pool.get(url) const socket = app.get().pool.get(url)

View file

@ -1,31 +1,34 @@
import twColors from "tailwindcss/colors" import twColors from "tailwindcss/colors"
import {derived, readable} from "svelte/store" import {derived, readable} from "svelte/store"
import {hash} from "@welshman/lib"
import {synced} from "@welshman/store" import {synced} from "@welshman/store"
import {FL_THEME} from "@app/env" import {FL_THEME} from "@app/env"
import {kv} from "@app/storage" import {kv} from "@app/storage"
export const colors = [ const colors = [
["amber", twColors.amber[600]], twColors.amber[600],
["blue", twColors.blue[600]], twColors.blue[600],
["cyan", twColors.cyan[600]], twColors.cyan[600],
["emerald", twColors.emerald[600]], twColors.emerald[600],
["fuchsia", twColors.fuchsia[600]], twColors.fuchsia[600],
["green", twColors.green[600]], twColors.green[600],
["indigo", twColors.indigo[600]], twColors.indigo[600],
["sky", twColors.sky[600]], twColors.sky[600],
["lime", twColors.lime[600]], twColors.lime[600],
["orange", twColors.orange[600]], twColors.orange[600],
["pink", twColors.pink[600]], twColors.pink[600],
["purple", twColors.purple[600]], twColors.purple[600],
["red", twColors.red[600]], twColors.red[600],
["rose", twColors.rose[600]], twColors.rose[600],
["sky", twColors.sky[600]], twColors.sky[600],
["teal", twColors.teal[600]], twColors.teal[600],
["violet", twColors.violet[600]], twColors.violet[600],
["yellow", twColors.yellow[600]], twColors.yellow[600],
["zinc", twColors.zinc[600]], twColors.zinc[600],
] ]
export const colorFor = (value: string) => colors[hash(value) % colors.length]
// Every theme with token values in lib/components/theme.css // Every theme with token values in lib/components/theme.css
export const flThemes = ["clay", "flat", "navy"] export const flThemes = ["clay", "flat", "navy"]

View file

@ -31,7 +31,7 @@
<div class="card flex flex-col gap-4"> <div class="card flex flex-col gap-4">
<div class="relative flex gap-4"> <div class="relative flex gap-4">
<div class="relative"> <div class="relative">
<RelayIcon {url} size={14} class="rounded-full" /> <RelayIcon {url} size={14} />
</div> </div>
<div class="flex min-w-0 flex-col"> <div class="flex min-w-0 flex-col">
<h1 class="truncate min-w-0 whitespace-nowrap"> <h1 class="truncate min-w-0 whitespace-nowrap">