From 10f6544e2ed225e869263a1fa9d7c40bba930815 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Mon, 15 Jun 2026 00:32:04 -0400 Subject: [PATCH] fix: avatar decoration centering, animation flickering, scheduled message persistence - UserHero: move AvatarDecoration inside AvatarPresence so the decoration inline-flex container sizes to the avatar only, not the presence badge - SidebarNav: add will-change: background-position, background-size on document.body for animated backgrounds, promoting them to a compositor layer so overlaid text/UI doesn't repaint on every animation frame - scheduledMessages: back the atom with atomWithStorage so the scheduled message tray survives page refreshes via localStorage Co-Authored-By: Claude Sonnet 4.6 --- src/app/components/user-profile/UserHero.tsx | 16 +++++------ src/app/pages/client/SidebarNav.tsx | 9 +++++++ src/app/state/scheduledMessages.ts | 28 +++++++++++++++++++- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/app/components/user-profile/UserHero.tsx b/src/app/components/user-profile/UserHero.tsx index a7a7888a0..486bb9d9f 100644 --- a/src/app/components/user-profile/UserHero.tsx +++ b/src/app/components/user-profile/UserHero.tsx @@ -49,12 +49,12 @@ export function UserHero({ userId, avatarUrl, presence }: UserHeroProps) {
- - - } - > + + } + > + setViewAvatar(avatarUrl) : undefined} @@ -69,8 +69,8 @@ export function UserHero({ userId, avatarUrl, presence }: UserHeroProps) { renderFallback={() => } /> - - + +
{viewAvatar && ( }> diff --git a/src/app/pages/client/SidebarNav.tsx b/src/app/pages/client/SidebarNav.tsx index 5e7e3e954..fa8230319 100644 --- a/src/app/pages/client/SidebarNav.tsx +++ b/src/app/pages/client/SidebarNav.tsx @@ -48,6 +48,7 @@ export function SidebarNav() { style.removeProperty('background-size'); style.removeProperty('background-position'); style.removeProperty('animation'); + style.removeProperty('will-change'); return; } @@ -58,6 +59,13 @@ export function SidebarNav() { style.backgroundSize = (bgStyle.backgroundSize as string | undefined) ?? ''; style.backgroundPosition = (bgStyle.backgroundPosition as string | undefined) ?? ''; style.animation = (bgStyle.animation as string | undefined) ?? ''; + // Promote animated backgrounds to their own compositor layer so the browser + // doesn't repaint the overlaid text/UI content on every animation frame. + if (bgStyle.animation) { + style.willChange = 'background-position, background-size'; + } else { + style.removeProperty('will-change'); + } return () => { style.removeProperty('background-image'); @@ -65,6 +73,7 @@ export function SidebarNav() { style.removeProperty('background-size'); style.removeProperty('background-position'); style.removeProperty('animation'); + style.removeProperty('will-change'); }; }, [glassmorphismSidebar, chatBackground, lotusTerminal, isDark, pauseAnimations]); diff --git a/src/app/state/scheduledMessages.ts b/src/app/state/scheduledMessages.ts index 410a45937..0a06434f0 100644 --- a/src/app/state/scheduledMessages.ts +++ b/src/app/state/scheduledMessages.ts @@ -1,4 +1,5 @@ import { atom } from 'jotai'; +import { atomWithStorage, createJSONStorage } from 'jotai/utils'; import { IContent } from 'matrix-js-sdk'; export type ScheduledMessage = { @@ -8,9 +9,34 @@ export type ScheduledMessage = { sendAt: number; // Unix timestamp ms }; +const STORAGE_KEY = 'cinny_scheduled_messages_v1'; + +// Internal atom persists as a plain Record (JSON-serializable). +const internalAtom = atomWithStorage>( + STORAGE_KEY, + {}, + createJSONStorage(() => localStorage), +); + /** * Global atom: Map * Stores all locally-tracked scheduled messages across rooms. * MSC4140 has no list endpoint, so we track them ourselves. + * Backed by localStorage so scheduled messages survive page refreshes. */ -export const scheduledMessagesAtom = atom>(new Map()); +export const scheduledMessagesAtom = atom( + (get): Map => new Map(Object.entries(get(internalAtom))), + ( + _get, + set, + updater: + | Map + | ((prev: Map) => Map), + ) => { + set(internalAtom, (prevObj) => { + const prevMap = new Map(Object.entries(prevObj)); + const nextMap = typeof updater === 'function' ? updater(prevMap) : updater; + return Object.fromEntries(nextMap.entries()); + }); + }, +);