diff --git a/src/features/dashboard/components/Greeting.tsx b/src/features/dashboard/components/Greeting.tsx index 91a7930..3ebfeb6 100644 --- a/src/features/dashboard/components/Greeting.tsx +++ b/src/features/dashboard/components/Greeting.tsx @@ -4,8 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAuthStore } from '@/stores/authStore'; +import { getAccountId, jmapQueryAndGet } from '@/services/jmap/client'; import inbuxaMark from '@/assets/inbuxa-mark.png'; function partOfDay(hour: number): 'morning' | 'afternoon' | 'evening' { @@ -14,13 +16,40 @@ function partOfDay(hour: number): 'morning' | 'afternoon' | 'evening' { return 'evening'; } -/** The dashboard's hello: whose server this is, and a nod to the time of day. */ +/** + * The name to greet: the account's full name where it has one, otherwise the + * name it signs in with. Looked up by the local part and matched on the whole + * address, so an account of the same name on another domain can't answer. + */ +function useFullName(username: string | null): string | null { + const [fullName, setFullName] = useState(null); + useEffect(() => { + if (!username) return; + let live = true; + const localPart = username.split('@')[0]; + jmapQueryAndGet('x:Account', getAccountId('x:Account'), { filter: { name: localPart } }, ['description', 'emailAddress']) + .then((responses) => { + const list = (responses[1]?.[1] as { list?: { description?: string | null; emailAddress?: string }[] }).list ?? []; + const own = list.find((a) => a.emailAddress?.toLowerCase() === username.toLowerCase()); + const name = own?.description?.trim(); + if (live && name) setFullName(name); + }) + .catch(() => { + /* the sign-in name stands */ + }); + return () => { + live = false; + }; + }, [username]); + return fullName; +} + +/** The dashboard's hello: to whoever is signed in, with a nod to the time of day. */ export function Greeting() { const { t } = useTranslation(); - const accounts = useAuthStore((s) => s.accounts); - const activeAccountId = useAuthStore((s) => s.activeAccountId); - const full = (activeAccountId && accounts[activeAccountId]?.name) || ''; - const name = full.split('@')[0]; + const username = useAuthStore((s) => s.username); + const fullName = useFullName(username); + const name = fullName ?? username?.split('@')[0] ?? ''; const part = partOfDay(new Date().getHours()); const hello = part === 'morning' @@ -36,7 +65,13 @@ export function Greeting() { {hello} {name && `, ${name}`} -

+

+ {username && ( + <> + {t('greeting.signedInAs', 'Signed in as {{username}}', { username })} + {' ยท '} + + )} {t('greeting.subtitle', "Here's how your mail server is doing.")}

diff --git a/src/i18n/en.json b/src/i18n/en.json index bbdf247..be197d3 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -389,6 +389,7 @@ "morning": "Good morning", "afternoon": "Good afternoon", "evening": "Good evening", - "subtitle": "Here's how your mail server is doing." + "subtitle": "Here's how your mail server is doing.", + "signedInAs": "Signed in as {{username}}" } } diff --git a/src/pages/AdminPanel.tsx b/src/pages/AdminPanel.tsx index 7324799..d0dc825 100644 --- a/src/pages/AdminPanel.tsx +++ b/src/pages/AdminPanel.tsx @@ -143,6 +143,7 @@ export default function AdminPanel() { if (cancelled) return; setSession(accounts, primaryAccountId, apiUrl, maxObjectsInGet, maxObjectsInSet); + useAuthStore.getState().setUsername(typeof session.username === 'string' ? session.username : null); const [schemaData, accountData] = await Promise.all([fetchSchema(), fetchAccountInfo()]); diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts index 34d15a3..6e1a79d 100644 --- a/src/stores/authStore.ts +++ b/src/stores/authStore.ts @@ -26,6 +26,8 @@ interface AuthState { apiUrl: string | null; maxObjectsInGet: number; maxObjectsInSet: number; + /** INBUXA: who is signed in, from the JMAP session (`username`). */ + username: string | null; setTokens: ( access: string, @@ -42,6 +44,7 @@ interface AuthState { maxObjectsInSet?: number, ) => void; switchAccount: (accountId: string) => void; + setUsername: (username: string | null) => void; logout: () => void; isAuthenticated: () => boolean; isTokenExpiringSoon: () => boolean; @@ -58,6 +61,11 @@ export const useAuthStore = create()( accounts: {}, primaryAccountId: null, activeAccountId: null, + username: null, + + setUsername: (username) => { + set({ username }); + }, apiUrl: null, maxObjectsInGet: 500, maxObjectsInSet: 500, @@ -103,6 +111,7 @@ export const useAuthStore = create()( primaryAccountId: null, activeAccountId: null, apiUrl: null, + username: null, }); },