From bae67616830bde6d33db71f5f106a2353b4724a0 Mon Sep 17 00:00:00 2001 From: heaven Date: Sun, 3 May 2026 13:22:10 +0300 Subject: [PATCH] feat(bots): render Matrix-native bot avatar in BotCard sidebar row and BotShellHero so server-side avatar_url propagates without client patches --- src/app/features/bots/BotCard.tsx | 29 ++++++++++++++++++++++---- src/app/features/bots/BotShell.css.ts | 20 +++++++++++++++++- src/app/features/bots/BotShellHero.tsx | 19 ++++++++++++++++- 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/app/features/bots/BotCard.tsx b/src/app/features/bots/BotCard.tsx index 3294080a..a2b95e78 100644 --- a/src/app/features/bots/BotCard.tsx +++ b/src/app/features/bots/BotCard.tsx @@ -1,7 +1,11 @@ import React from 'react'; -import { Avatar, Box, Text, toRem } from 'folds'; +import { Avatar, AvatarImage, Box, Text, toRem } from 'folds'; import { NavItem, NavItemContent, NavLink } from '../../components/nav'; import { getBotPath } from '../../pages/pathUtils'; +import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; +import { useUserProfile } from '../../hooks/useUserProfile'; +import { mxcUrlToHttp } from '../../utils/matrix'; import type { BotPreset } from './catalog'; const MONO_FONT = '"JetBrains Mono Variable", ui-monospace, monospace'; @@ -14,8 +18,21 @@ type BotCardProps = { }; export function BotCard({ preset, selected }: BotCardProps) { + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); const initial = preset.name.trim().charAt(0).toUpperCase() || '?'; + // Standard Matrix avatar resolution. `useUserProfile` returns the cached + // profile synchronously and subscribes to live `UserEvent.AvatarUrl` + // updates — when the operator sets the bot's `avatar_url` server-side + // (mautrix-telegram bridge config), every BotCard remount picks it up + // without client deploys. The fleet-blue letter square remains as the + // fallback when the bot has no profile avatar yet. + const { avatarUrl: avatarMxc } = useUserProfile(preset.mxid); + const avatarUrl = avatarMxc + ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 56, 56, 'crop') ?? undefined + : undefined; + return ( - - {initial} - + {avatarUrl ? ( + + ) : ( + + {initial} + + )} covers +// the violet — `overflow: hidden` keeps it inside the rounded corners. export const HeroAvatar = style([ DefaultReset, { @@ -114,6 +117,7 @@ export const HeroAvatar = style([ alignItems: 'center', justifyContent: 'center', flexShrink: 0, + overflow: 'hidden', '@media': { '(max-width: 600px)': { @@ -126,6 +130,20 @@ export const HeroAvatar = style([ }, ]); +// Avatar image fills the violet square. `objectFit: cover` plus the +// container's `overflow: hidden` means non-square Matrix avatars (which +// arbitrarily sized) render correctly without showing the violet bg +// around the corners. +export const HeroAvatarImg = style([ + DefaultReset, + { + width: '100%', + height: '100%', + objectFit: 'cover', + display: 'block', + }, +]); + export const HeroBody = style([ DefaultReset, { diff --git a/src/app/features/bots/BotShellHero.tsx b/src/app/features/bots/BotShellHero.tsx index de824555..b28f57f3 100644 --- a/src/app/features/bots/BotShellHero.tsx +++ b/src/app/features/bots/BotShellHero.tsx @@ -8,6 +8,9 @@ import { BotShellMenu } from './BotShellMenu'; import { BackRouteHandler } from '../../components/BackRouteHandler'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; import { stopPropagation } from '../../utils/keyboard'; +import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; +import { mxcUrlToHttp } from '../../utils/matrix'; import * as css from './BotShell.css'; type BotShellHeroProps = { @@ -28,10 +31,24 @@ const heroInitial = (preset: BotPreset): string => { export function BotShellHero({ preset, room }: BotShellHeroProps) { const { t } = useTranslation(); + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); const screenSize = useScreenSizeContext(); const isMobile = screenSize === ScreenSize.Mobile; const [menuAnchor, setMenuAnchor] = useState(); + // Standard Matrix avatar resolution. The bot user's profile carries the + // canonical `avatar_url` (set server-side by the bridge or homeserver + // admin); every Matrix client reads the same source. We pull it via the + // bot's RoomMember in this DM rather than `mx.getUser()` because the + // member event in this room is guaranteed to be loaded by the time we + // render — we wouldn't be on `/bots/` otherwise — and avoids any + // edge case where `mx.getUser()` returns a stale or missing entry. + const avatarMxc = room.getMember(preset.mxid)?.getMxcAvatarUrl(); + const avatarUrl = avatarMxc + ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined + : undefined; + const handleOpenMenu: React.MouseEventHandler = (evt) => { setMenuAnchor(evt.currentTarget.getBoundingClientRect()); }; @@ -66,7 +83,7 @@ export function BotShellHero({ preset, room }: BotShellHeroProps) { )}