This commit is contained in:
Timothy Jaeryang Baek committed 2026-07-16 01:10:13 -04:00
1 parent c55e373b99
commit 200d447f62
3 files changed
+353 -228

No files matched your search

+15 -6
View File
@@ -42,10 +42,15 @@ def _normalize_target(target: dict[str, Any], existing: dict[str, Any] | None =
validate_url(url) validate_url(url)
config['url'] = url config['url'] = url
events = target.get('events', existing.get('events') or sorted(VALID_EVENTS)) events = target['events'] if 'events' in target else existing.get('events', [])
events = [str(event) for event in events if str(event) in VALID_EVENTS] if events is None:
if not events: events = []
raise ValueError('At least one notification event is required') if not isinstance(events, list):
raise ValueError('events must be a list')
events = [str(event) for event in events]
unsupported = [event for event in events if event not in VALID_EVENTS]
if unsupported:
raise ValueError(f'unsupported notification event: {unsupported[0]}')
delivery = str(target.get('delivery') or existing.get('delivery') or 'away') delivery = str(target.get('delivery') or existing.get('delivery') or 'away')
if delivery not in VALID_DELIVERY: if delivery not in VALID_DELIVERY:
@@ -83,7 +88,11 @@ async def _load_notifications(user_id: str) -> dict[str, Any]:
targets = notifications.get('targets') targets = notifications.get('targets')
if not isinstance(targets, list) or not targets: if not isinstance(targets, list) or not targets:
legacy_url = str(settings.get('ui', {}).get('notifications', {}).get('webhook_url') or '').strip() legacy_url = str(
notifications.get('webhook_url')
or settings.get('ui', {}).get('notifications', {}).get('webhook_url')
or ''
).strip()
if legacy_url: if legacy_url:
target = _normalize_target( target = _normalize_target(
{ {
@@ -96,7 +105,7 @@ async def _load_notifications(user_id: str) -> dict[str, Any]:
'config': {'url': legacy_url}, 'config': {'url': legacy_url},
} }
) )
notifications = {'targets': [target], 'default_target_id': DEFAULT_TARGET_ID} notifications = {**notifications, 'targets': [target], 'default_target_id': DEFAULT_TARGET_ID}
await Users.update_user_settings_by_id(user_id, {'notifications': notifications}) await Users.update_user_settings_by_id(user_id, {'notifications': notifications})
else: else:
notifications['targets'] = [target for target in targets if isinstance(target, dict)] notifications['targets'] = [target for target in targets if isinstance(target, dict)]
@@ -4,11 +4,9 @@
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { config, settings, user } from '$lib/stores'; import { config, settings, user } from '$lib/stores';
import Modal from '$lib/components/common/Modal.svelte';
import Switch from '$lib/components/common/Switch.svelte'; import Switch from '$lib/components/common/Switch.svelte';
import SettingsSelect from '$lib/components/common/SettingsSelect.svelte'; import Plus from '$lib/components/icons/Plus.svelte';
import UserSettingField from './UserSettingField.svelte';
import UserSettingRow from './UserSettingRow.svelte';
import UserSettingSection from './UserSettingSection.svelte';
import { import {
createNotificationTarget, createNotificationTarget,
deleteNotificationTarget, deleteNotificationTarget,
@@ -26,40 +24,54 @@
let notificationEnabled = false; let notificationEnabled = false;
let notificationSound = true; let notificationSound = true;
let notificationSoundAlways = false;
let targets: NotificationTarget[] = []; let targets: NotificationTarget[] = [];
let defaultTargetId: string | null = null; let defaultTargetId: string | null = null;
let events: { event: string; label: string }[] = []; let events: { event: string; label: string; description?: string }[] = [
let loading = false; {
let editingTarget: NotificationTarget | null = null; event: 'chat.finished',
let targetName = 'Webhook'; label: 'Chat finished',
let targetUrl = ''; description: 'A chat run finished successfully.'
let targetEnabled = true; },
let targetDelivery: 'away' | 'always' = 'away'; { event: 'chat.failed', label: 'Chat failed', description: 'A chat run failed.' }
let targetEvents = ['chat.finished', 'chat.failed']; ];
let loadingTargets = false;
const inputClass = let savingTarget = false;
'h-7 w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500'; let editingId: string | null = null;
const actionButtonClass = let formOpen = false;
'text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white disabled:cursor-not-allowed disabled:opacity-50'; let form = {
id: '',
url: '',
enabled: true,
events: [] as string[],
delivery: 'away' as 'away' | 'always'
};
let loadedTargets = false;
$: canUseWebhooks = $: canUseWebhooks =
($config?.features as any)?.enable_user_webhooks && ($config?.features as any)?.enable_user_webhooks &&
($user?.role === 'admin' || ($user?.permissions?.features?.webhooks ?? false)); ($user?.role === 'admin' || ($user?.permissions?.features?.webhooks ?? false));
const setNotificationEnabled = async (enabled: boolean) => { $: if (canUseWebhooks && !loadedTargets) {
const permission = enabled ? await Notification.requestPermission() : 'granted'; void loadTargets();
}
if (permission === 'granted') { const toggleNotifications = async () => {
notificationEnabled = enabled; if (!notificationEnabled) {
saveSettings({ notificationEnabled }); const permission =
'Notification' in window ? await Notification.requestPermission() : 'denied';
if (permission === 'granted') {
notificationEnabled = true;
saveSettings({ notificationEnabled });
} else {
toast.error(
$i18n.t(
'Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.'
)
);
}
} else { } else {
notificationEnabled = false; notificationEnabled = false;
toast.error( saveSettings({ notificationEnabled });
$i18n.t(
'Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.'
)
);
} }
}; };
@@ -68,75 +80,110 @@
return; return;
} }
loading = true; loadingTargets = true;
loadedTargets = true;
try { try {
events = await getNotificationEvents(localStorage.token); const [eventResult, targetResult] = await Promise.allSettled([
const response = await getNotificationTargets(localStorage.token); getNotificationEvents(localStorage.token),
targets = response.targets ?? []; getNotificationTargets(localStorage.token)
defaultTargetId = response.default_target_id ?? null; ]);
if (eventResult.status === 'fulfilled' && eventResult.value?.length) {
events = eventResult.value;
}
if (targetResult.status === 'fulfilled') {
targets = targetResult.value.targets ?? [];
defaultTargetId = targetResult.value.default_target_id ?? null;
} else {
toast.error(`${targetResult.reason}`);
}
} catch (error) { } catch (error) {
toast.error(`${error}`); toast.error(`${error}`);
} finally { } finally {
loading = false; loadingTargets = false;
} }
}; };
const resetForm = () => { const openNewTarget = () => {
editingTarget = null; editingId = null;
targetName = 'Webhook'; formOpen = true;
targetUrl = ''; form = {
targetEnabled = true; id: '',
targetDelivery = 'away'; url: '',
targetEvents = ['chat.finished', 'chat.failed']; enabled: true,
events: [],
delivery: 'away'
};
}; };
const editTarget = (target: NotificationTarget) => { const openEditTarget = (target: NotificationTarget) => {
editingTarget = target; editingId = target.id;
targetName = target.name; formOpen = true;
targetUrl = ''; form = {
targetEnabled = target.enabled; id: target.id,
targetDelivery = target.delivery; url: '',
targetEvents = [...target.events]; enabled: target.enabled,
events: [...target.events],
delivery: target.delivery
};
};
const toggleFormEvent = (event: string) => {
form.events = form.events.includes(event)
? form.events.filter((item) => item !== event)
: [...form.events, event];
}; };
const saveTarget = async () => { const saveTarget = async () => {
savingTarget = true;
try { try {
const id = form.id.trim();
const payload: Partial<NotificationTarget> = { const payload: Partial<NotificationTarget> = {
...(id ? { id, name: id } : {}),
type: 'webhook', type: 'webhook',
name: targetName, enabled: form.enabled,
enabled: targetEnabled, events: form.events,
events: targetEvents, delivery: form.delivery,
delivery: targetDelivery, ...(form.url.trim() ? { config: { url: form.url.trim() } } : {})
config: targetUrl ? { url: targetUrl } : {}
}; };
if (editingTarget) { if (editingId) {
await updateNotificationTarget(localStorage.token, editingTarget.id, payload); await updateNotificationTarget(localStorage.token, editingId, payload);
} else { } else {
await createNotificationTarget(localStorage.token, payload); await createNotificationTarget(localStorage.token, payload);
} }
await loadTargets();
formOpen = false;
toast.success($i18n.t('Settings saved successfully!')); toast.success($i18n.t('Settings saved successfully!'));
resetForm(); } catch (error) {
toast.error(`${error}`);
} finally {
savingTarget = false;
}
};
const patchTarget = async (target: NotificationTarget, patch: Partial<NotificationTarget>) => {
try {
await updateNotificationTarget(localStorage.token, target.id, patch);
await loadTargets(); await loadTargets();
} catch (error) { } catch (error) {
toast.error(`${error}`); toast.error(`${error}`);
} }
}; };
const toggleEvent = (event: string) => { const sendTest = async (target: NotificationTarget) => {
if (targetEvents.includes(event)) { try {
targetEvents = targetEvents.filter((item) => item !== event); await testNotificationTarget(localStorage.token, target.id);
} else { toast.success($i18n.t('Test notification sent.'));
targetEvents = [...targetEvents, event]; } catch (error) {
toast.error(`${error}`);
} }
}; };
onMount(async () => { onMount(async () => {
notificationEnabled = $settings.notificationEnabled ?? false; notificationEnabled = $settings.notificationEnabled ?? false;
notificationSound = $settings?.notificationSound ?? true; notificationSound = $settings?.notificationSound ?? true;
notificationSoundAlways = $settings?.notificationSoundAlways ?? false;
await loadTargets();
}); });
</script> </script>
@@ -146,24 +193,25 @@
{$i18n.t('Notifications')} {$i18n.t('Notifications')}
</h2> </h2>
<UserSettingSection title={$i18n.t('Local Notifications')} first> <div class="flex flex-col gap-2.5">
<UserSettingRow <label class="flex cursor-pointer items-center justify-between">
label={$i18n.t('Browser Notifications')} <span class="text-xs text-gray-600 dark:text-gray-400">
description={$i18n.t('Allow browser notifications for completed responses.')} {$i18n.t('Browser Notifications')}
> </span>
<Switch <Switch
state={notificationEnabled} state={notificationEnabled}
ariaLabel={$i18n.t('Browser Notifications')} ariaLabel={$i18n.t('Browser Notifications')}
on:change={(event) => { on:change={toggleNotifications}
setNotificationEnabled(event.detail);
}}
/> />
</UserSettingRow> </label>
<p class="-mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t('Allow browser notifications for completed responses.')}
</p>
<UserSettingRow <label class="flex cursor-pointer items-center justify-between">
label={$i18n.t('Notification Sound')} <span class="text-xs text-gray-600 dark:text-gray-400">
description={$i18n.t('Play a sound when new chat activity arrives.')} {$i18n.t('Notification Sound')}
> </span>
<Switch <Switch
bind:state={notificationSound} bind:state={notificationSound}
ariaLabel={$i18n.t('Notification Sound')} ariaLabel={$i18n.t('Notification Sound')}
@@ -171,175 +219,242 @@
saveSettings({ notificationSound }); saveSettings({ notificationSound });
}} }}
/> />
</UserSettingRow> </label>
{#if notificationSound} {#if canUseWebhooks}
<UserSettingRow <div class="mt-3 flex items-center justify-between">
label={$i18n.t('Always Play Notification Sound')} <span class="text-xs font-medium text-gray-700 dark:text-gray-300">
description={$i18n.t('Play notification sounds even when the app is focused.')} {$i18n.t('Notification Targets')}
> </span>
<Switch <button
bind:state={notificationSoundAlways} class="flex h-6 w-6 items-center justify-center rounded-lg text-gray-400 transition-colors duration-75 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300"
ariaLabel={$i18n.t('Always Play Notification Sound')} type="button"
on:change={() => { on:click={openNewTarget}
saveSettings({ notificationSoundAlways }); title={$i18n.t('Add Notification Target')}
}} aria-label={$i18n.t('Add Notification Target')}
/> >
</UserSettingRow> <Plus className="size-3.5" strokeWidth="2" />
{/if} </button>
</UserSettingSection> </div>
{#if canUseWebhooks} {#if loadingTargets}
<UserSettingSection title={$i18n.t('Webhook Targets')}> <p class="text-[0.6875rem] text-gray-400 dark:text-gray-600">{$i18n.t('Loading...')}</p>
{#if loading} {:else if !targets.length}
<div class="text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Loading...')}</div> <p class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
{:else if targets.length === 0}
<div class="text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('No notification targets configured.')} {$i18n.t('No notification targets configured.')}
</div> </p>
{:else} {:else}
<div class="flex flex-col gap-2"> <div class="flex flex-col">
{#each targets as target} {#each targets as target}
<div {@const alertLabels = events
class="rounded-lg border border-gray-100/70 p-2.5 text-xs dark:border-white/[0.06]" .filter((event) => target.events.includes(event.event))
> .map((event) => event.label)
<div class="flex items-center justify-between gap-3"> .join(', ')}
<div class="min-w-0"> <div class="notification-target-row">
<div class="truncate font-medium text-gray-700 dark:text-gray-300"> <div class="min-w-0 flex-1">
{target.name} <div class="flex min-w-0 items-center gap-2">
{#if target.id === defaultTargetId} <span class="truncate text-[0.71875rem] text-gray-700 dark:text-gray-300">
<span class="ml-1 text-gray-400 dark:text-gray-600" {target.id}
>{$i18n.t('Default')}</span </span>
> <span class="shrink-0 text-[0.625rem] text-gray-400 dark:text-gray-600">
{/if} {$i18n.t('Webhook')}
</div> </span>
<div class="truncate text-gray-400 dark:text-gray-600"> {#if target.id === defaultTargetId}
{target.config?.url} <span class="shrink-0 text-[0.625rem] text-gray-400 dark:text-gray-600">
</div> {$i18n.t('Default')}
</span>
{/if}
</div>
<div
class="truncate text-[0.625rem] leading-tight text-gray-400 dark:text-gray-600"
>
{target.config?.url}
</div>
<div
class="truncate text-[0.625rem] leading-tight text-gray-400 dark:text-gray-600"
>
{alertLabels || $i18n.t('No chat alerts')}
{#if target.events.length}
&middot; {target.delivery === 'away'
? $i18n.t('Only when away')
: $i18n.t('Always')}
{/if}
</div> </div>
<Switch
state={target.enabled}
ariaLabel={$i18n.t('Enabled')}
on:change={async (event) => {
await updateNotificationTarget(localStorage.token, target.id, {
enabled: event.detail
});
await loadTargets();
}}
/>
</div> </div>
<div class="mt-2 flex flex-wrap gap-2 text-gray-400 dark:text-gray-600"> <div class="flex shrink-0 items-center gap-2">
<span
>{target.delivery === 'always'
? $i18n.t('Always')
: $i18n.t('Only when away')}</span
>
<span>{target.events.join(', ')}</span>
</div>
<div class="mt-2 flex flex-wrap gap-3">
<button <button
class={actionButtonClass} class="text-[0.625rem] text-gray-400 transition-colors duration-100 hover:text-gray-600 dark:hover:text-gray-300"
type="button" type="button"
on:click={async () => { on:click={() => sendTest(target)}
await testNotificationTarget(localStorage.token, target.id);
toast.success($i18n.t('Test notification sent.'));
}}>{$i18n.t('Test')}</button
> >
{$i18n.t('Send Test')}
</button>
{#if target.id !== defaultTargetId}
<button
class="text-[0.625rem] text-gray-400 transition-colors duration-100 hover:text-gray-600 dark:hover:text-gray-300"
type="button"
on:click={async () => {
await setDefaultNotificationTarget(localStorage.token, target.id);
await loadTargets();
}}
>
{$i18n.t('Make Default')}
</button>
{/if}
<button <button
class={actionButtonClass} class="text-[0.625rem] text-gray-400 transition-colors duration-100 hover:text-gray-600 dark:hover:text-gray-300"
type="button" type="button"
on:click={() => editTarget(target)}>{$i18n.t('Edit')}</button on:click={() => openEditTarget(target)}
> >
{$i18n.t('Edit')}
</button>
<button <button
class={actionButtonClass} class="text-[0.625rem] text-gray-400 transition-colors duration-100 hover:text-gray-600 dark:hover:text-gray-300"
type="button"
disabled={target.id === defaultTargetId}
on:click={async () => {
await setDefaultNotificationTarget(localStorage.token, target.id);
await loadTargets();
}}>{$i18n.t('Make Default')}</button
>
<button
class={actionButtonClass}
type="button" type="button"
on:click={async () => { on:click={async () => {
await deleteNotificationTarget(localStorage.token, target.id); await deleteNotificationTarget(localStorage.token, target.id);
await loadTargets(); await loadTargets();
}}>{$i18n.t('Remove')}</button }}
> >
{$i18n.t('Remove')}
</button>
</div>
<div class="flex w-9 shrink-0 justify-end">
<Switch
state={target.enabled}
ariaLabel={$i18n.t('Enabled')}
on:change={(event) => patchTarget(target, { enabled: event.detail })}
/>
</div> </div>
</div> </div>
{/each} {/each}
</div> </div>
{/if} {/if}
</UserSettingSection> {/if}
</div>
<UserSettingSection title={editingTarget ? $i18n.t('Edit Target') : $i18n.t('Add Target')}>
<UserSettingField label={$i18n.t('Name')}>
<input class={inputClass} type="text" bind:value={targetName} />
</UserSettingField>
<UserSettingField
label={$i18n.t('Webhook URL')}
description={editingTarget
? $i18n.t('Leave blank to keep the current webhook URL.')
: $i18n.t('Send chat notifications to this webhook URL.')}
>
<input
class={inputClass}
type="url"
bind:value={targetUrl}
placeholder={editingTarget
? editingTarget.config?.url
: $i18n.t('Enter your webhook URL')}
/>
</UserSettingField>
<UserSettingRow label={$i18n.t('Enabled')}>
<Switch bind:state={targetEnabled} ariaLabel={$i18n.t('Enabled')} />
</UserSettingRow>
<UserSettingField label={$i18n.t('Delivery')}>
<SettingsSelect bind:value={targetDelivery} ariaLabel={$i18n.t('Delivery')}>
<option value="away">{$i18n.t('Only when away')}</option>
<option value="always">{$i18n.t('Always')}</option>
</SettingsSelect>
</UserSettingField>
<UserSettingField label={$i18n.t('Events')}>
<div class="flex flex-col gap-1.5">
{#each events as item}
<label class="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
<input
type="checkbox"
checked={targetEvents.includes(item.event)}
on:change={() => toggleEvent(item.event)}
/>
<span>{$i18n.t(item.label)}</span>
</label>
{/each}
</div>
</UserSettingField>
<div class="flex justify-end gap-3">
{#if editingTarget}
<button class={actionButtonClass} type="button" on:click={resetForm}
>{$i18n.t('Cancel')}</button
>
{/if}
<button
class="rounded-full bg-black px-3.5 py-1.5 text-sm font-normal text-white transition hover:bg-gray-900 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-black dark:hover:bg-gray-100"
type="button"
disabled={!editingTarget && !targetUrl}
on:click={saveTarget}
>
{editingTarget ? $i18n.t('Save') : $i18n.t('Add')}
</button>
</div>
</UserSettingSection>
{/if}
</div> </div>
</div> </div>
<Modal size="sm" bind:show={formOpen}>
<div class="p-4">
<h2 class="mb-3 text-sm font-medium text-gray-900 dark:text-white">
{editingId ? $i18n.t('Edit') : $i18n.t('Add Notification Target')}
</h2>
<div class="mb-2 flex gap-1">
<button
class="h-7 rounded-md bg-gray-200/60 px-2 text-xs text-gray-900 transition-colors dark:bg-white/10 dark:text-white"
type="button"
>
{$i18n.t('Webhook')}
</button>
</div>
<div class="text-[0.625rem] text-gray-400 dark:text-gray-600">
{$i18n.t('Target ID for notify')}
</div>
<input
bind:value={form.id}
placeholder={$i18n.t('Target ID')}
autocomplete="off"
spellcheck="false"
class="block w-full bg-transparent py-0.5 font-mono text-[0.8125rem] text-gray-700 outline-none placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
/>
<div class="mt-2 text-[0.625rem] text-gray-400 dark:text-gray-600">
{$i18n.t('Webhook')}
</div>
<input
type="url"
bind:value={form.url}
placeholder={editingId
? $i18n.t('Keep current webhook URL')
: 'https://hooks.slack.com/services/...'}
autocomplete="off"
spellcheck="false"
class="block w-full bg-transparent py-0.5 font-mono text-[0.8125rem] text-gray-700 outline-none placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
/>
<div class="mt-3 mb-1 text-[0.625rem] text-gray-400 dark:text-gray-600">
{$i18n.t('Automatic Events')}
</div>
<div class="flex flex-wrap gap-1">
{#each events as event}
<button
class="h-6 rounded-md px-2 text-[0.6875rem] transition-colors {form.events.includes(
event.event
)
? 'bg-gray-200/60 text-gray-900 dark:bg-white/10 dark:text-white'
: 'text-gray-500 hover:text-gray-800 dark:hover:text-gray-300'}"
type="button"
on:click={() => toggleFormEvent(event.event)}
>
{event.label}
</button>
{/each}
</div>
{#if form.events.length}
<div class="mt-3 mb-1 text-[0.625rem] text-gray-400 dark:text-gray-600">
{$i18n.t('Automatic Delivery')}
</div>
<div class="flex gap-1">
{#each ['away', 'always'] as mode}
<button
class="h-6 rounded-md px-2 text-[0.6875rem] transition-colors {form.delivery === mode
? 'bg-gray-200/60 text-gray-900 dark:bg-white/10 dark:text-white'
: 'text-gray-500 hover:text-gray-800 dark:hover:text-gray-300'}"
type="button"
on:click={() => (form.delivery = mode as 'away' | 'always')}
>
{mode === 'away' ? $i18n.t('Only when away') : $i18n.t('Always')}
</button>
{/each}
</div>
{/if}
<p class="mt-3 text-[0.625rem] text-gray-400 dark:text-gray-600">
{$i18n.t(
'The notify tool always sends to an enabled target, regardless of automatic event settings.'
)}
</p>
<div class="mt-4 flex justify-end gap-3">
<button
class="text-[0.8125rem] text-gray-400 transition-colors duration-100 hover:text-gray-600 dark:hover:text-gray-300"
type="button"
on:click={() => (formOpen = false)}
>
{$i18n.t('Cancel')}
</button>
<button
class="text-[0.8125rem] text-gray-700 transition-colors duration-100 hover:text-gray-900 disabled:opacity-30 dark:text-gray-300 dark:hover:text-white"
type="button"
on:click={saveTarget}
disabled={savingTarget || (!editingId && !form.url.trim())}
>
{savingTarget ? $i18n.t('Saving...') : $i18n.t('Save')}
</button>
</div>
</div>
</Modal>
<style>
.notification-target-row {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.375rem 0.25rem;
border-bottom: 1px solid rgb(229 231 235 / 0.7);
}
.notification-target-row:last-child {
border-bottom: none;
}
:global(.dark) .notification-target-row {
border-bottom-color: rgb(255 255 255 / 0.06);
}
</style>
+2 -1
View File
@@ -30,6 +30,7 @@
import WrenchAlt from '../icons/WrenchAlt.svelte'; import WrenchAlt from '../icons/WrenchAlt.svelte';
import Face from '../icons/Face.svelte'; import Face from '../icons/Face.svelte';
import AppNotification from '../icons/AppNotification.svelte'; import AppNotification from '../icons/AppNotification.svelte';
import AdjustmentsHorizontal from '../icons/AdjustmentsHorizontal.svelte';
import ArchiveBox from '../icons/ArchiveBox.svelte'; import ArchiveBox from '../icons/ArchiveBox.svelte';
import ChevronLeft from '../icons/ChevronLeft.svelte'; import ChevronLeft from '../icons/ChevronLeft.svelte';
import Keyboard from '../icons/Keyboard.svelte'; import Keyboard from '../icons/Keyboard.svelte';
@@ -914,7 +915,7 @@
selectedTab = 'interface'; selectedTab = 'interface';
}} }}
> >
<AppNotification className="size-3.5" strokeWidth="2" /> <AdjustmentsHorizontal className="size-3.5" strokeWidth="2" />
<span>{$i18n.t('Interface')}</span> <span>{$i18n.t('Interface')}</span>
</button> </button>
{:else if tabId === 'notifications'} {:else if tabId === 'notifications'}