Give Sneedchat its brand mark, and drop rail entries nothing can reach
The account entry is the home for buffers no backend grouped, which is how IRC, Sneedchat and Matrix reach the rail at all. Discord never uses it - every channel belongs to a guild and every DM to the DM entry - so it drew a tile that opened an empty pane. Emptiness alone is not grounds for hiding one: IRC and Sneedchat have no buffers either until they finish connecting, and Sneedchat's Tor bootstrap makes that several seconds. So an entry survives if something is under it, or if its account has no buffers anywhere yet, and only one whose buffers all live elsewhere is dropped. The rail and the channel pane share that rule rather than each applying its own - if they disagreed the selected group could be one with no tile, leaving a pane with no way out of it. Sneedchat replaces its vpn_lock placeholder with the real mark. The supplied artwork was a screenshot with the background baked in, which would have masked to a solid square, so the background is keyed out to alpha - keeping the eye as a hole so the silhouette still reads. Since serviceIcon's artwork is no longer always an SVG, its field is renamed to `mark`, and a `colour` flag marks artwork worth showing unflattened: the rail has room and already sits beside full-colour guild icons, while the same file still masks to a monochrome glyph in the buffer rows.
This commit is contained in:
1 parent
6a285a8807
commit
decd239750
7 files changed
+86
-14
No files matched your search
@@ -1735,3 +1735,13 @@ video.lightbox-media {
|
||||
min-width: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* A brand mark kept in its own colours. Unlike a guild icon it is a logo
|
||||
rather than a photo, so it is fitted with breathing room instead of cropped
|
||||
to fill the tile. */
|
||||
.rail-mark {
|
||||
width: 74%;
|
||||
height: 74%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -38,7 +38,7 @@ export function AccountsPanel(): JSX.Element {
|
||||
className={`service-chip${adding === service ? ' active' : ''}`}
|
||||
onClick={() => setAdding(adding === service ? null : service)}
|
||||
>
|
||||
{icon.svg ? <MaskIcon src={icon.svg} size={16} /> : <Icon name={icon.glyph!} size={16} />}
|
||||
{icon.mark ? <MaskIcon src={icon.mark} size={16} /> : <Icon name={icon.glyph!} size={16} />}
|
||||
{serviceLabel(service)}
|
||||
</button>
|
||||
)
|
||||
@@ -72,8 +72,8 @@ function AccountRow({ account }: { account: Account }): JSX.Element {
|
||||
<div className="account-card-head">
|
||||
{account.avatarUrl ? (
|
||||
<img className="account-avatar" src={resolveMediaUrl(account.avatarUrl)} alt="" />
|
||||
) : icon.svg ? (
|
||||
<MaskIcon src={icon.svg} size={20} />
|
||||
) : icon.mark ? (
|
||||
<MaskIcon src={icon.mark} size={20} />
|
||||
) : (
|
||||
<Icon name={icon.glyph!} size={20} />
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMemo } from 'react'
|
||||
import { Icon, IconButton, MaskIcon } from './Icon'
|
||||
import { ContextMenu, useContextMenu, type MenuEntry } from './ContextMenu'
|
||||
import { useChat, useIdSetPref, usePref, useStore } from '../state/hooks'
|
||||
import { visibleGroups } from '../lib/groups'
|
||||
import type { BufferEntry } from '../state/store'
|
||||
import {
|
||||
bufferDisplayName,
|
||||
@@ -38,7 +39,8 @@ export function BufferList(): JSX.Element {
|
||||
|
||||
// A stale selection (a guild that went away, a first run) resolves to the
|
||||
// first entry rather than an empty pane.
|
||||
const activeGroup = groups.find((g) => g.id === activeGroupId) || groups[0]
|
||||
const shownGroups = useMemo(() => visibleGroups(groups, visible), [groups, visible])
|
||||
const activeGroup = shownGroups.find((g) => g.id === activeGroupId) || shownGroups[0]
|
||||
const groupAccount = accounts.find((a) => a.id === activeGroup?.accountId)
|
||||
|
||||
// Server buffer first, then most recent activity - the ordering nobilis's
|
||||
@@ -243,8 +245,8 @@ function BufferRow({
|
||||
// name as text in front of every entry.
|
||||
const service = serviceIcon(account?.service ?? '')
|
||||
const leading = showServiceIcon ? (
|
||||
service.svg ? (
|
||||
<MaskIcon src={service.svg} size={15} />
|
||||
service.mark ? (
|
||||
<MaskIcon src={service.mark} size={15} />
|
||||
) : (
|
||||
<Icon name={service.glyph!} size={15} />
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Icon, MaskIcon } from './Icon'
|
||||
import { useChat, useStore } from '../state/hooks'
|
||||
import { nickColor, resolveMediaUrl, serviceIcon } from '../lib/util'
|
||||
import { visibleGroups } from '../lib/groups'
|
||||
import type { BufferGroup } from '../../../shared/wire'
|
||||
import type { BufferEntry } from '../state/store'
|
||||
|
||||
@@ -41,8 +42,13 @@ function RailTile({ group, active, unread, highlight, onSelect }: TileProps): JS
|
||||
content = <img className="rail-icon" src={resolveMediaUrl(group.iconUrl)} alt="" />
|
||||
} else if (group.kind === 'dms') {
|
||||
content = <Icon name="forum" size={22} />
|
||||
} else if (group.kind === 'account' && service.mark && service.colour) {
|
||||
// Artwork with colour worth keeping, shown as-is rather than flattened to
|
||||
// a silhouette - it sits beside full-colour guild icons here, and this is
|
||||
// the one place with room for it.
|
||||
content = <img className="rail-mark" src={service.mark} alt="" />
|
||||
} else if (group.kind === 'account') {
|
||||
content = service.svg ? <MaskIcon src={service.svg} size={22} /> : <Icon name={service.glyph!} size={22} />
|
||||
content = service.mark ? <MaskIcon src={service.mark} size={22} /> : <Icon name={service.glyph!} size={22} />
|
||||
} else {
|
||||
content = (
|
||||
<span className="rail-initials" style={{ color: nickColor(group.name) }}>
|
||||
@@ -82,8 +88,6 @@ export function ServerRail(): JSX.Element | null {
|
||||
const activeGroupId = useChat((s) => s.activeGroupId)
|
||||
const buffers = useChat((s) => s.buffers)
|
||||
|
||||
if (groups.length <= 1) return null
|
||||
|
||||
// Unread rolls up from the buffers under each entry, so a guild whose
|
||||
// channels are all collapsed away still shows it has something waiting.
|
||||
const totals = new Map<string, { unread: number; highlight: boolean }>()
|
||||
@@ -95,9 +99,13 @@ export function ServerRail(): JSX.Element | null {
|
||||
totals.set(b.groupId, t)
|
||||
}
|
||||
|
||||
const visible = visibleGroups(groups, buffers as BufferEntry[])
|
||||
|
||||
if (visible.length <= 1) return null
|
||||
|
||||
return (
|
||||
<nav className="server-rail" aria-label="Servers">
|
||||
{groups.map((g) => {
|
||||
{visible.map((g) => {
|
||||
const t = totals.get(g.id)
|
||||
return (
|
||||
<RailTile
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { BufferGroup } from '../../../shared/wire'
|
||||
import type { BufferEntry } from '../state/store'
|
||||
|
||||
/**
|
||||
* The rail entries worth drawing.
|
||||
*
|
||||
* An account entry is the home for buffers no backend grouped, which is how
|
||||
* IRC, Sneedchat and Matrix reach the rail at all. Discord never puts anything
|
||||
* there - every channel belongs to a guild and every DM to the DM entry - so
|
||||
* its account entry would be a tile that opens an empty pane.
|
||||
*
|
||||
* Emptiness alone is not enough to hide one: IRC and Sneedchat have no buffers
|
||||
* either until they finish connecting, and Sneedchat's Tor bootstrap makes
|
||||
* that several seconds. So an account with nothing anywhere yet keeps its
|
||||
* tile, and only one whose buffers all live elsewhere loses it.
|
||||
*
|
||||
* Shared by the rail and the channel pane deliberately - if they disagreed,
|
||||
* the selected group could be one with no tile, leaving a pane the user cannot
|
||||
* navigate away from.
|
||||
*/
|
||||
export function visibleGroups(groups: BufferGroup[], buffers: BufferEntry[]): BufferGroup[] {
|
||||
const inGroup = new Map<string, number>()
|
||||
const inAccount = new Map<string, number>()
|
||||
for (const b of buffers) {
|
||||
inAccount.set(b.accountId, (inAccount.get(b.accountId) ?? 0) + 1)
|
||||
if (b.groupId) inGroup.set(b.groupId, (inGroup.get(b.groupId) ?? 0) + 1)
|
||||
}
|
||||
return groups.filter((g) => {
|
||||
if (g.kind !== 'account') return true
|
||||
if ((inGroup.get(g.id) ?? 0) > 0) return true
|
||||
return (inAccount.get(g.accountId) ?? 0) === 0
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import discordSvg from '../assets/discord.svg'
|
||||
import sneedchatMark from '../assets/sneedchat.png'
|
||||
import matrixSvg from '../assets/matrix.svg'
|
||||
|
||||
/**
|
||||
@@ -26,12 +27,30 @@ export function guildOf(name: string): string {
|
||||
* XMPP (which has no widely recognised mark), and "vpn_lock" evokes the
|
||||
* Tor-only transport Sneedchat runs over.
|
||||
*/
|
||||
export function serviceIcon(service: string): { svg?: string; glyph?: string } {
|
||||
/**
|
||||
* A service's brand mark.
|
||||
*
|
||||
* `mark` is artwork whose *alpha* is the silhouette, so it works as a CSS mask
|
||||
* regardless of being an SVG or a PNG - which is what lets these sit quietly in
|
||||
* a list, painted in the surrounding text colour like any other icon.
|
||||
*
|
||||
* `colour` says the artwork's own colours are worth keeping where there is
|
||||
* room for them - the rail, where it sits beside full-colour guild icons -
|
||||
* while the same file still masks down to a monochrome glyph in the compact
|
||||
* places.
|
||||
*/
|
||||
export interface ServiceIcon {
|
||||
mark?: string
|
||||
colour?: boolean
|
||||
glyph?: string
|
||||
}
|
||||
|
||||
export function serviceIcon(service: string): ServiceIcon {
|
||||
switch (service) {
|
||||
case 'discord':
|
||||
return { svg: discordSvg }
|
||||
return { mark: discordSvg }
|
||||
case 'matrix':
|
||||
return { svg: matrixSvg }
|
||||
return { mark: matrixSvg }
|
||||
case 'irc':
|
||||
return { glyph: 'tag' }
|
||||
case 'slack':
|
||||
@@ -40,7 +59,7 @@ export function serviceIcon(service: string): { svg?: string; glyph?: string } {
|
||||
case 'xmpp':
|
||||
return { glyph: 'forum' }
|
||||
case 'sockchat':
|
||||
return { glyph: 'vpn_lock' }
|
||||
return { mark: sneedchatMark, colour: true }
|
||||
default:
|
||||
return { glyph: 'chat' }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user