mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:41:09 +02:00
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:
parent
83c75ce90e
commit
2521a1ea73
7 changed files with 256 additions and 42 deletions
|
|
@ -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
|
||||
|
|
|
|||
77
apps/matrix/apps/web/src/lib/stores/userSettings.svelte.ts
Normal file
77
apps/matrix/apps/web/src/lib/stores/userSettings.svelte.ts
Normal 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,
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -264,6 +264,7 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe
|
|||
...settings.general?.startPages,
|
||||
},
|
||||
},
|
||||
recentEmojis: settings.recentEmojis ?? globalSettings.recentEmojis,
|
||||
};
|
||||
saveToStorage();
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue