feat(bots): render Matrix-native bot avatar in BotCard sidebar row and BotShellHero so server-side avatar_url propagates without client patches
This commit is contained in:
parent
316c3eb9fd
commit
bae6761683
3 changed files with 62 additions and 6 deletions
|
|
@ -1,7 +1,11 @@
|
||||||
import React from 'react';
|
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 { NavItem, NavItemContent, NavLink } from '../../components/nav';
|
||||||
import { getBotPath } from '../../pages/pathUtils';
|
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';
|
import type { BotPreset } from './catalog';
|
||||||
|
|
||||||
const MONO_FONT = '"JetBrains Mono Variable", ui-monospace, monospace';
|
const MONO_FONT = '"JetBrains Mono Variable", ui-monospace, monospace';
|
||||||
|
|
@ -14,8 +18,21 @@ type BotCardProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function BotCard({ preset, selected }: BotCardProps) {
|
export function BotCard({ preset, selected }: BotCardProps) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const useAuthentication = useMediaAuthentication();
|
||||||
const initial = preset.name.trim().charAt(0).toUpperCase() || '?';
|
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 (
|
return (
|
||||||
<NavItem
|
<NavItem
|
||||||
variant="Background"
|
variant="Background"
|
||||||
|
|
@ -37,9 +54,13 @@ export function BotCard({ preset, selected }: BotCardProps) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Avatar size="300" radii="400" style={{ background: AVATAR_BG, color: '#0c0c0e' }}>
|
<Avatar size="300" radii="400" style={{ background: AVATAR_BG, color: '#0c0c0e' }}>
|
||||||
<Text as="span" size="H6" style={{ color: '#0c0c0e', fontWeight: 700 }}>
|
{avatarUrl ? (
|
||||||
{initial}
|
<AvatarImage src={avatarUrl} alt={preset.name} />
|
||||||
</Text>
|
) : (
|
||||||
|
<Text as="span" size="H6" style={{ color: '#0c0c0e', fontWeight: 700 }}>
|
||||||
|
{initial}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Box
|
<Box
|
||||||
as="span"
|
as="span"
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,10 @@ export const HeroBack = style([
|
||||||
|
|
||||||
// 56×56 square avatar with 14px radius, fleet violet (DAWN.fleet) bg.
|
// 56×56 square avatar with 14px radius, fleet violet (DAWN.fleet) bg.
|
||||||
// Fleet color is hardcoded here because it's the canonical bot accent in
|
// Fleet color is hardcoded here because it's the canonical bot accent in
|
||||||
// the mockup and we don't want it varying with Folds palette swaps.
|
// the mockup and we don't want it varying with Folds palette swaps. The
|
||||||
|
// violet square shows when the bot's Matrix profile has no `avatar_url`
|
||||||
|
// (fallback to the initial letter); when it does, the inner <img> covers
|
||||||
|
// the violet — `overflow: hidden` keeps it inside the rounded corners.
|
||||||
export const HeroAvatar = style([
|
export const HeroAvatar = style([
|
||||||
DefaultReset,
|
DefaultReset,
|
||||||
{
|
{
|
||||||
|
|
@ -114,6 +117,7 @@ export const HeroAvatar = style([
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
|
||||||
'@media': {
|
'@media': {
|
||||||
'(max-width: 600px)': {
|
'(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([
|
export const HeroBody = style([
|
||||||
DefaultReset,
|
DefaultReset,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ import { BotShellMenu } from './BotShellMenu';
|
||||||
import { BackRouteHandler } from '../../components/BackRouteHandler';
|
import { BackRouteHandler } from '../../components/BackRouteHandler';
|
||||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||||
import { stopPropagation } from '../../utils/keyboard';
|
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';
|
import * as css from './BotShell.css';
|
||||||
|
|
||||||
type BotShellHeroProps = {
|
type BotShellHeroProps = {
|
||||||
|
|
@ -28,10 +31,24 @@ const heroInitial = (preset: BotPreset): string => {
|
||||||
|
|
||||||
export function BotShellHero({ preset, room }: BotShellHeroProps) {
|
export function BotShellHero({ preset, room }: BotShellHeroProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const useAuthentication = useMediaAuthentication();
|
||||||
const screenSize = useScreenSizeContext();
|
const screenSize = useScreenSizeContext();
|
||||||
const isMobile = screenSize === ScreenSize.Mobile;
|
const isMobile = screenSize === ScreenSize.Mobile;
|
||||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||||
|
|
||||||
|
// 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/<id>` 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<HTMLButtonElement> = (evt) => {
|
const handleOpenMenu: React.MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||||
setMenuAnchor(evt.currentTarget.getBoundingClientRect());
|
setMenuAnchor(evt.currentTarget.getBoundingClientRect());
|
||||||
};
|
};
|
||||||
|
|
@ -66,7 +83,7 @@ export function BotShellHero({ preset, room }: BotShellHeroProps) {
|
||||||
</BackRouteHandler>
|
</BackRouteHandler>
|
||||||
)}
|
)}
|
||||||
<div className={css.HeroAvatar} aria-hidden="true">
|
<div className={css.HeroAvatar} aria-hidden="true">
|
||||||
{initial}
|
{avatarUrl ? <img className={css.HeroAvatarImg} src={avatarUrl} alt="" /> : initial}
|
||||||
</div>
|
</div>
|
||||||
<div className={css.HeroBody}>
|
<div className={css.HeroBody}>
|
||||||
<div className={css.HeroTitleRow}>
|
<div className={css.HeroTitleRow}>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue