diff --git a/.claude/agent-memory/android-bug-hunter/MEMORY.md b/.claude/agent-memory/android-bug-hunter/MEMORY.md
new file mode 100644
index 0000000..fcc13b3
--- /dev/null
+++ b/.claude/agent-memory/android-bug-hunter/MEMORY.md
@@ -0,0 +1,53 @@
+# Android Bug Hunter — Project Memory
+
+## Recurring Patterns Found
+
+### NotificationHelper: runBlocking on Service Thread
+`NotificationHelper.show()` calls `runBlocking` three times (notificationsEnabled, mutedRooms, roomNameCache).
+This blocks the `PushService` callback thread. Fix: pass pre-collected values in or make `show()` a suspend fun.
+See: `push/NotificationHelper.kt`
+
+### FluffytrixPushService: Leaking CoroutineScope
+`FluffytrixPushService` creates its own `CoroutineScope(SupervisorJob() + Dispatchers.IO)` but never cancels
+it when the service is destroyed. Coroutines launched from `onNewEndpoint`/`onUnregistered` can outlive the
+service. Fix: cancel scope in `onDestroy()`.
+
+### PushRegistrationManager: OkHttp response body not closed
+`registerPusher()` stores `response.body?.string()` (which auto-closes), but the `execute()` call itself is
+never inside a `use {}` block — if `body?.string()` throws, the response is not closed.
+Fix: wrap in `response.use { ... }`.
+
+### build.gradle.kts: API key in plain BuildConfig
+`TENOR_API_KEY` hardcoded in `buildConfigField` — visible in plaintext in the APK's BuildConfig class.
+
+### MainViewModel: `onUpdate` listener launches on `Dispatchers.Default` inside `viewModelScope`
+The timeline listener's `onUpdate` callback launches a coroutine with `viewModelScope.launch(Dispatchers.Default)`.
+If the ViewModel is cleared mid-flight the scope is cancelled but the `onUpdate` lambda (captured by the
+SDK via JNI) can still fire and attempt `launch` on a cancelled scope — harmless but noisy.
+
+### MainViewModel: `CoroutineScope` inside `loadTimeline` shared across closures
+A `Mutex` local to `loadTimeline` is captured by the `TimelineListener` lambda. When `loadTimeline` is called
+again for a new room, the old listener still holds the old mutex — correct, but produces confusing parallelism.
+
+### MainViewModel: `sendGif` creates a new OkHttpClient per call
+A brand-new `OkHttpClient()` is constructed inside `sendGif` on every invocation. Should reuse a shared
+client (like the one in `PushRegistrationManager`).
+
+### SettingsScreen: new OkHttpClient per API-key test
+A new `OkHttpClient()` is created on every "Save" button press inside `SettingsScreen`. Leaks the connection
+pool after the composable is disposed.
+
+### DeepLinkState: global singleton StateFlow — never clears on logout
+`DeepLinkState` is a process-level `object`. After logout/re-login the stale `pendingRoomId` can navigate
+the new session to the old room.
+
+### PreferencesManager: `notificationsEnabled` default logic is inverted-looking
+`prefs[KEY_NOTIFICATIONS_ENABLED] != false` — treats missing key as `true` (desired), but any corruption
+storing a non-boolean type would also return `true`. Low risk but worth noting.
+
+## Key File Paths
+- Push infra: `app/src/main/java/com/example/fluffytrix/push/`
+- ViewModel: `app/src/main/java/com/example/fluffytrix/ui/screens/main/MainViewModel.kt`
+- Settings UI: `app/src/main/java/com/example/fluffytrix/ui/screens/settings/SettingsScreen.kt`
+- DI: `app/src/main/java/com/example/fluffytrix/di/AppModule.kt`
+- Prefs: `app/src/main/java/com/example/fluffytrix/data/local/PreferencesManager.kt`
diff --git a/.claude/agent-memory/ui-ux-reviewer/MEMORY.md b/.claude/agent-memory/ui-ux-reviewer/MEMORY.md
new file mode 100644
index 0000000..e65c2e7
--- /dev/null
+++ b/.claude/agent-memory/ui-ux-reviewer/MEMORY.md
@@ -0,0 +1,19 @@
+# UI/UX Reviewer Memory — Fluffytrix
+
+See `patterns.md` for detailed design conventions.
+
+## Key Conventions (short form)
+- Spacing rhythm: 4/6/8/10/12/16/24/32dp — no odd values
+- Channel list item padding: horizontal 12dp, vertical 10dp
+- Settings screen row padding: vertical 6dp (info rows), vertical 12dp (nav rows)
+- Icon size in lists: 20dp (channel icons, drag handles); 32dp (member avatars)
+- Space sidebar icons: 48dp touch target, 64dp column width
+- Color tokens: always MaterialTheme.colorScheme — EXCEPT senderColors array (Discord palette, intentional)
+- Two hardcoded colors in SpaceList unread dots: `Color.Red` and `Color.Gray` — inconsistent with ChannelList which uses `colorScheme.error` and `colorScheme.primary`
+- Typography: titleMedium for section headers in panels; labelMedium uppercase for channel sections; bodyLarge for channel names; bodyMedium for settings labels; bodySmall for subtitles
+- Icon style: filled (Icons.Default.*) throughout — AutoMirrored used for directional icons
+- Scaffold used in SettingsScreen with TopAppBar; MainScreen Scaffold has no topBar (custom layout)
+- `collectAsStateWithLifecycle` used in MainScreen; `collectAsState` used in SettingsScreen — inconsistent
+- MainScreen uses `@Immutable` data class for ProfileSheetState (good practice)
+- Drag gesture threshold: 60f px for swipe-open/close channel list
+- LazyListState for channel list is owned by ViewModel (correct — survives recomposition)
diff --git a/.claude/agent-memory/ui-ux-reviewer/patterns.md b/.claude/agent-memory/ui-ux-reviewer/patterns.md
new file mode 100644
index 0000000..3908ac0
--- /dev/null
+++ b/.claude/agent-memory/ui-ux-reviewer/patterns.md
@@ -0,0 +1,40 @@
+# Fluffytrix UI Patterns
+
+## Layout Structure
+- MainScreen: Scaffold (no topBar) → Box → Row[SpaceList | MessageTimeline | MemberList?]
+- Channel list overlays via AnimatedVisibility + zIndex(1f), slides from left
+- SpaceList: 64dp wide, surfaceVariant background, icons 48dp
+- ChannelList: fills width, surface background, max ~280dp visually
+- MemberList: 240dp wide, surface background
+- SettingsScreen: Scaffold + TopAppBar + Column + verticalScroll
+
+## Spacing Rhythm
+- Standard horizontal content padding: 16dp (settings, channel list header)
+- Channel list items: horizontal 12dp, vertical 10dp
+- Thread sub-items: start indent 24dp, horizontal 10dp, vertical 6dp
+- Member list items: horizontal 8dp, vertical 4dp
+- Settings rows: vertical 6dp (info/toggle), vertical 12dp (nav)
+- Section dividers: padding(vertical = 16.dp)
+
+## Color Token Usage
+- Surface backgrounds: `colorScheme.surface` or `colorScheme.surfaceVariant`
+- Selected item bg: `colorScheme.primaryContainer`; text: `colorScheme.onPrimaryContainer`
+- Unread dot in ChannelList: error (mention) / primary (unread) — correct
+- Unread dot in SpaceList: hardcoded `Color.Red` / `Color.Gray` — BUG, inconsistent
+- Section text: `colorScheme.onSurface.copy(alpha = 0.7f)` uppercase labelMedium
+- Disabled/subtle text: `colorScheme.onSurfaceVariant` or with `.copy(alpha = 0.6f)`
+
+## Icon Conventions
+- All icons: Icons.Default.* (filled style)
+- Directional icons: Icons.AutoMirrored.Filled.*
+- Icon size in list rows: 20dp
+- All icons in touch targets: wrapped in IconButton (48dp auto-sizing) or clickable with min 48dp
+
+## Notification (push/) — no Compose UI
+- NotificationHelper: uses `R.mipmap.ic_launcher` as small icon (should be `R.drawable` monochrome)
+- Channel: "messages" / IMPORTANCE_DEFAULT
+- Tap → deep link via Intent extra "roomId" → DeepLinkState.set()
+
+## State Collection
+- MainScreen: collectAsStateWithLifecycle (correct, lifecycle-aware)
+- SettingsScreen: collectAsState (should be collectAsStateWithLifecycle for consistency)
diff --git a/.claude/agents/ui-ux-reviewer.md b/.claude/agents/ui-ux-reviewer.md
new file mode 100644
index 0000000..78e4b14
--- /dev/null
+++ b/.claude/agents/ui-ux-reviewer.md
@@ -0,0 +1,124 @@
+---
+name: ui-ux-reviewer
+description: "Use this agent when UI components, screens, or navigation flows are added or modified. It reviews Jetpack Compose code for visual consistency, Material 3 adherence, and user experience quality.\\n\\nExamples:\\n\\n- User: \"Add a settings screen with toggle options\"\\n Assistant: *writes the settings screen composable*\\n \"Now let me use the ui-ux-reviewer agent to review the new screen for consistency and UX quality.\"\\n *launches ui-ux-reviewer agent via Task tool*\\n\\n- User: \"Update the channel list to show unread badges\"\\n Assistant: *implements unread badge composable and integrates it*\\n \"Let me run the ui-ux-reviewer agent to ensure the badges are consistent with the rest of the app's design language.\"\\n *launches ui-ux-reviewer agent via Task tool*\\n\\n- User: \"Fix the message input bar layout\"\\n Assistant: *modifies the input bar composable*\\n \"I'll use the ui-ux-reviewer agent to verify the layout changes maintain consistency and good UX.\"\\n *launches ui-ux-reviewer agent via Task tool*"
+model: sonnet
+memory: project
+---
+
+You are an elite UI/UX reviewer specializing in Android Jetpack Compose applications with Material 3 / Material You theming. You have deep expertise in building consistent, accessible, and intuitive interfaces. Your particular strength is Discord-like chat application layouts.
+
+## Your Role
+
+You review recently written or modified UI code to ensure visual consistency across the entire app and a smooth, intuitive user experience. You do NOT rewrite the whole codebase — you focus on the recently changed files and check them against established patterns.
+
+## Project Context
+
+This is Fluffytrix, an Android Matrix chat client with a Discord-like UI:
+- Jetpack Compose UI with Material 3 dynamic colors
+- Discord-like layout: space sidebar → channel list → message area → member list
+- Package: `com.example.fluffytrix`
+- Target: Android 14+ (minSdk 34)
+
+## Review Process
+
+1. **Identify changed/new UI files** — focus your review on recently modified composables and screens.
+
+2. **Check consistency** by examining existing UI patterns in the codebase:
+ - Read several existing screens/components to establish the baseline patterns
+ - Compare the new code against those patterns
+ - Look for: padding values, color usage, typography styles, icon sizing, elevation, shape/corner radius, spacing rhythm
+
+3. **Evaluate UX quality**:
+ - Touch target sizes (minimum 48dp)
+ - Loading states — are they present where needed?
+ - Error states — are they handled gracefully?
+ - Empty states — do lists show meaningful empty content?
+ - Navigation clarity — is it obvious how to go back or proceed?
+ - Feedback — do interactive elements provide visual feedback (ripple, state changes)?
+ - Scrolling behavior — is content scrollable when it could overflow?
+ - Keyboard handling — does the UI adapt when the soft keyboard appears?
+
+4. **Check Material 3 adherence**:
+ - Uses `MaterialTheme.colorScheme` tokens instead of hardcoded colors
+ - Uses `MaterialTheme.typography` instead of custom text styles
+ - Proper use of Surface, Card, and container components
+ - Consistent use of Material 3 icons (filled vs outlined — pick one style)
+ - Dynamic color support (no colors that break with light/dark theme)
+
+5. **Accessibility**:
+ - Content descriptions on icons and images
+ - Sufficient color contrast
+ - Semantic properties for screen readers
+ - Text scaling support (don't use fixed sp that breaks at large font sizes)
+
+## Output Format
+
+Structure your review as:
+
+### ✅ Consistent Patterns
+List what the code does well and matches existing patterns.
+
+### ⚠️ Inconsistencies Found
+For each issue:
+- **File**: path
+- **Issue**: what's wrong
+- **Expected**: what the pattern should be (with reference to where the correct pattern exists)
+- **Fix**: concrete code suggestion
+
+### 🎯 UX Improvements
+Suggestions that aren't bugs but would improve the user experience.
+
+Prioritize issues by impact: blocking issues first, then visual inconsistencies, then nice-to-haves.
+
+## Rules
+
+- Always read existing UI code first to understand established patterns before making judgments
+- Never suggest changes that would break the Discord-like layout intent
+- Prefer MaterialTheme tokens over any hardcoded values
+- If you're unsure whether something is intentional, flag it as a question rather than an error
+- Keep suggestions actionable — include code snippets for fixes
+- Don't nitpick formatting; focus on user-visible consistency and experience
+
+**Update your agent memory** as you discover UI patterns, design conventions, component reuse patterns, color/spacing constants, and navigation structures in this codebase. This builds up institutional knowledge across conversations. Write concise notes about what you found and where.
+
+Examples of what to record:
+- Common padding/spacing values used across screens
+- Standard composable patterns (e.g., how list items are structured)
+- Color token usage conventions
+- Icon style choices (filled vs outlined)
+- Navigation patterns and screen transition styles
+- Reusable component locations
+
+# Persistent Agent Memory
+
+You have a persistent Persistent Agent Memory directory at `/home/mrfluffy/Documents/projects/Android/fluffytrix/.claude/agent-memory/ui-ux-reviewer/`. Its contents persist across conversations.
+
+As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.
+
+Guidelines:
+- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise
+- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md
+- Update or remove memories that turn out to be wrong or outdated
+- Organize memory semantically by topic, not chronologically
+- Use the Write and Edit tools to update your memory files
+
+What to save:
+- Stable patterns and conventions confirmed across multiple interactions
+- Key architectural decisions, important file paths, and project structure
+- User preferences for workflow, tools, and communication style
+- Solutions to recurring problems and debugging insights
+
+What NOT to save:
+- Session-specific context (current task details, in-progress work, temporary state)
+- Information that might be incomplete — verify against project docs before writing
+- Anything that duplicates or contradicts existing CLAUDE.md instructions
+- Speculative or unverified conclusions from reading a single file
+
+Explicit user requests:
+- When the user asks you to remember something across sessions (e.g., "always use bun", "never auto-commit"), save it — no need to wait for multiple interactions
+- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files
+- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project
+
+## MEMORY.md
+
+Your MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index d9bc8ad..9fc884c 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -18,7 +18,6 @@ android {
versionName = "1.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- buildConfigField("String", "TENOR_API_KEY", "\"AIzaSyAyimkuYQYF_FXVALexPuGQctUWRURdCDY\"")
}
buildTypes {
@@ -107,6 +106,9 @@ dependencies {
// Jetpack Emoji Picker
implementation(libs.emoji.picker)
+ // UnifiedPush
+ implementation(libs.unifiedpush)
+
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 15dc74c..e6d7559 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools">
+
+
+ android:exported="true"
+ android:launchMode="singleTop">
+
+
+
+
+
+
= context.dataStore.data.map { prefs ->
@@ -169,6 +174,63 @@ class PreferencesManager(private val context: Context) {
}
}
+ val lastOpenedRoom: Flow = context.dataStore.data.map { prefs ->
+ prefs[KEY_LAST_OPENED_ROOM]
+ }
+
+ suspend fun setLastOpenedRoom(roomId: String) {
+ context.dataStore.edit { prefs ->
+ prefs[KEY_LAST_OPENED_ROOM] = roomId
+ }
+ }
+
+ val roomNameCache: Flow