feat(matrix): sync recent emojis across apps via mana-core-auth

- Add recentEmojis field to GlobalSettings in shared-theme
- Create userSettings store for Matrix app with JWT token management
- Exchange session cookie for JWT after SSO login
- Update MessageInput to use userSettings instead of localStorage
- Add recentEmojis support to mana-core-auth settings API

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-02-14 11:30:17 +01:00
parent 83c75ce90e
commit 2521a1ea73
7 changed files with 256 additions and 42 deletions

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { matrixStore, type SimpleMessage, type RoomMember } from '$lib/matrix';
import { userSettings } from '$lib/stores/userSettings.svelte';
import {
PaperPlaneTilt,
Paperclip,
@ -47,59 +48,121 @@
// Emoji picker state
let showEmojiPicker = $state(false);
const RECENT_EMOJIS_KEY = 'matrix_recent_emojis';
const MAX_RECENT_EMOJIS = 16; // 2 rows of 8
// Load recent emojis from localStorage
function loadRecentEmojis(): string[] {
if (typeof localStorage === 'undefined') return [];
try {
const stored = localStorage.getItem(RECENT_EMOJIS_KEY);
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
}
// Recent emojis from user settings (synced across apps)
let recentEmojis = $derived(userSettings.globalSettings?.recentEmojis ?? []);
// Save recent emojis to localStorage
function saveRecentEmojis(emojis: string[]) {
if (typeof localStorage === 'undefined') return;
try {
localStorage.setItem(RECENT_EMOJIS_KEY, JSON.stringify(emojis));
} catch {
// Ignore storage errors
}
}
// Add emoji to recent list
// Add emoji to recent list (saves to mana-core-auth)
function addToRecentEmojis(emoji: string) {
const recent = loadRecentEmojis();
const current = userSettings.globalSettings?.recentEmojis ?? [];
// Remove if already exists, then add to front
const filtered = recent.filter((e) => e !== emoji);
const filtered = current.filter((e) => e !== emoji);
const updated = [emoji, ...filtered].slice(0, MAX_RECENT_EMOJIS);
saveRecentEmojis(updated);
recentEmojis = updated;
// Update server (optimistic update handled by store)
userSettings.updateGlobal({ recentEmojis: updated });
}
let recentEmojis = $state<string[]>([]);
// Load recent emojis on mount (browser only)
$effect(() => {
recentEmojis = loadRecentEmojis();
});
const commonEmojis = [
// Smileys
'😀', '😃', '😄', '😁', '😅', '😂', '🤣', '😊', '😇', '🙂', '😉', '😌',
'😍', '🥰', '😘', '😗', '😙', '😚', '😋', '😛', '😜', '🤪', '😝', '🤗',
'🤭', '🤫', '🤔', '🤐', '🤨', '😐', '😑', '😶', '😏', '😒', '🙄', '😬',
'😮', '🤯', '😳', '🥺', '😢', '😭', '😤', '😠', '😡', '🤬', '😈', '👿',
'😀',
'😃',
'😄',
'😁',
'😅',
'😂',
'🤣',
'😊',
'😇',
'🙂',
'😉',
'😌',
'😍',
'🥰',
'😘',
'😗',
'😙',
'😚',
'😋',
'😛',
'😜',
'🤪',
'😝',
'🤗',
'🤭',
'🤫',
'🤔',
'🤐',
'🤨',
'😐',
'😑',
'😶',
'😏',
'😒',
'🙄',
'😬',
'😮',
'🤯',
'😳',
'🥺',
'😢',
'😭',
'😤',
'😠',
'😡',
'🤬',
'😈',
'👿',
// Gestures
'👍', '👎', '👌', '🤌', '✌️', '🤞', '🤟', '🤘', '🤙', '👋', '🖐️', '✋',
'👏', '🙌', '👐', '🤲', '🙏', '💪', '🦾', '❤️', '🧡', '💛', '💚', '💙',
'👍',
'👎',
'👌',
'🤌',
'✌️',
'🤞',
'🤟',
'🤘',
'🤙',
'👋',
'🖐️',
'✋',
'👏',
'🙌',
'👐',
'🤲',
'🙏',
'💪',
'🦾',
'❤️',
'🧡',
'💛',
'💚',
'💙',
// Objects & Symbols
'🔥', '✨', '💫', '⭐', '🌟', '💯', '💢', '💥', '💦', '💨', '🎉', '🎊',
'🎁', '🏆', '🥇', '🎯', '💡', '📌', '📍', '✅', '❌', '⚠️', '❗', '❓',
'🔥',
'✨',
'💫',
'⭐',
'🌟',
'💯',
'💢',
'💥',
'💦',
'💨',
'🎉',
'🎊',
'🎁',
'🏆',
'🥇',
'🎯',
'💡',
'📌',
'📍',
'✅',
'❌',
'⚠️',
'❗',
'❓',
];
function insertEmoji(emoji: string) {
@ -657,7 +720,9 @@
<!-- Recent/Frequently used emojis -->
{#if recentEmojis.length > 0}
<div class="mb-2">
<p class="text-[10px] text-muted-foreground uppercase font-medium px-1 mb-1">Häufig benutzt</p>
<p class="text-[10px] text-muted-foreground uppercase font-medium px-1 mb-1">
Häufig benutzt
</p>
<div class="grid grid-cols-8 gap-1">
{#each recentEmojis as emoji}
<button

View file

@ -0,0 +1,77 @@
import { createUserSettingsStore } from '@manacore/shared-theme';
import { browser } from '$app/environment';
const AUTH_URL = 'https://auth.mana.how';
const TOKEN_STORAGE_KEY = 'mana_core_access_token';
// Internal access token state
let accessToken: string | null = null;
/**
* Set the access token (called after SSO token exchange)
*/
export function setAccessToken(token: string): void {
accessToken = token;
if (browser) {
try {
localStorage.setItem(TOKEN_STORAGE_KEY, token);
} catch {
// Ignore storage errors
}
}
}
/**
* Clear the access token (called on logout)
*/
export function clearAccessToken(): void {
accessToken = null;
if (browser) {
try {
localStorage.removeItem(TOKEN_STORAGE_KEY);
} catch {
// Ignore storage errors
}
}
}
/**
* Load access token from localStorage (for page reloads)
*/
export function loadStoredAccessToken(): string | null {
if (!browser) return null;
try {
const stored = localStorage.getItem(TOKEN_STORAGE_KEY);
if (stored) {
accessToken = stored;
return stored;
}
} catch {
// Ignore storage errors
}
return null;
}
/**
* Get the current access token
*/
async function getAccessToken(): Promise<string | null> {
// If we have a token in memory, return it
if (accessToken) return accessToken;
// Try to load from storage
return loadStoredAccessToken();
}
/**
* User settings store for the Matrix app
*
* This store syncs settings with mana-core-auth and provides:
* - Global settings (including recentEmojis)
* - localStorage caching for offline support
*/
export const userSettings = createUserSettingsStore({
appId: 'matrix',
authUrl: AUTH_URL,
getAccessToken,
});

View file

@ -7,6 +7,12 @@
import type { Snippet } from 'svelte';
import { CircleNotch, WarningCircle, ArrowsClockwise } from '@manacore/shared-icons';
import { theme } from '$lib/stores/theme';
import {
userSettings,
setAccessToken,
clearAccessToken,
loadStoredAccessToken,
} from '$lib/stores/userSettings.svelte';
import {
THEME_DEFINITIONS,
DEFAULT_THEME_VARIANTS,
@ -23,6 +29,51 @@
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { setLocale, supportedLocales } from '$lib/i18n';
const AUTH_URL = 'https://auth.mana.how';
/**
* Exchange session cookie for JWT token from mana-core-auth
* This enables cross-app settings sync after Matrix SSO login
*/
async function fetchManaCoreToken(): Promise<boolean> {
try {
const response = await fetch(`${AUTH_URL}/api/v1/auth/session-to-token`, {
method: 'POST',
credentials: 'include', // Send session cookie
headers: {
'Content-Type': 'application/json',
},
});
if (response.ok) {
const data = await response.json();
if (data.accessToken) {
setAccessToken(data.accessToken);
return true;
}
}
} catch (e) {
console.warn('Could not exchange session for token:', e);
}
return false;
}
/**
* Initialize user settings (load from mana-core-auth)
*/
async function initUserSettings(): Promise<void> {
// First try to load stored token
const storedToken = loadStoredAccessToken();
// If no stored token, try to exchange session cookie
if (!storedToken) {
await fetchManaCoreToken();
}
// Load user settings (will use the token we just set)
await userSettings.load();
}
// App switcher items
const appItems = getPillAppItems('matrix');
@ -120,6 +171,7 @@
function handleLogout() {
matrixStore.logout();
clearAccessToken();
goto('/login');
}
@ -140,6 +192,8 @@
// Check if already initialized
if (matrixStore.isReady) {
// Matrix ready, initialize user settings in background
initUserSettings();
loading = false;
return;
}
@ -166,6 +220,10 @@
if (!initialized) {
initError = matrixStore.error || 'Failed to initialize Matrix client';
} else {
// Matrix ready after SSO, fetch mana-core-auth token and load settings
// This happens after SSO so the session cookie should be available
initUserSettings();
}
loading = false;
@ -193,6 +251,9 @@
}
// Has credentials but failed to init
initError = matrixStore.error || 'Failed to connect to Matrix server';
} else {
// Matrix ready, initialize user settings in background
initUserSettings();
}
loading = false;

View file

@ -292,6 +292,8 @@ export interface GlobalSettings {
locale: string;
/** General preferences (start pages, sounds, etc.) */
general: GeneralSettings;
/** Recently used emojis (shared across all apps) - max 16 */
recentEmojis?: string[];
}
/**
@ -363,6 +365,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
theme: { mode: 'system', colorScheme: 'ocean', pinnedThemes: [] },
locale: 'de',
general: DEFAULT_GENERAL_SETTINGS,
recentEmojis: [],
};
/**

View file

@ -264,6 +264,7 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe
...settings.general?.startPages,
},
},
recentEmojis: settings.recentEmojis ?? globalSettings.recentEmojis,
};
saveToStorage();

View file

@ -55,6 +55,11 @@ export class UpdateGlobalSettingsDto {
@IsOptional()
@IsString()
locale?: string;
@IsOptional()
@IsArray()
@IsString({ each: true })
recentEmojis?: string[];
}
// App override update
@ -115,6 +120,7 @@ export interface GlobalSettings {
nav: NavSettings;
theme: ThemeSettings;
locale: string;
recentEmojis?: string[];
}
export interface AppOverride {

View file

@ -91,6 +91,7 @@ export class SettingsService {
nav: { ...current.globalSettings.nav, ...dto.nav },
theme: { ...current.globalSettings.theme, ...dto.theme },
locale: dto.locale ?? current.globalSettings.locale,
recentEmojis: dto.recentEmojis ?? current.globalSettings.recentEmojis,
};
// Update in database