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