feat: Rename to Toju and add translation
Some checks failed
Deploy Web Apps / deploy (push) Successful in 5m52s
Build Android APK / build-android-apk (push) Failing after 23m15s
Queue Release Build / prepare (push) Successful in 1m42s
Queue Release Build / build-linux (push) Failing after 9m33s
Queue Release Build / build-windows (push) Successful in 26m5s
Queue Release Build / finalize (push) Has been skipped

This commit is contained in:
2026-06-05 17:13:03 +02:00
parent 8ecfc9a1fe
commit ee293d7daf
301 changed files with 8247 additions and 2218 deletions

View File

@@ -21,6 +21,7 @@ import { selectCurrentUser } from '../../../../store/users/users.selectors';
import { DesktopNotificationService } from '../../infrastructure/services/desktop-notification.service';
import { NotificationSettingsStorageService } from '../../infrastructure/services/notification-settings-storage.service';
import { createDefaultNotificationSettings, type NotificationsSettings } from '../../domain/models/notification.model';
import { initializeAppI18nForTests, provideAppI18nForTests } from '../../../../core/i18n/app-i18n.testing';
import { NotificationsService } from './notifications.service';
const alice: User = {
@@ -165,6 +166,7 @@ function createServiceContext(options: ServiceContextOptions): ServiceContext {
const injector = Injector.create({
providers: [
...provideAppI18nForTests(),
{
provide: DatabaseService,
useValue: {
@@ -228,6 +230,8 @@ function createServiceContext(options: ServiceContextOptions): ServiceContext {
]
});
initializeAppI18nForTests(injector);
return {
service: runInInjectionContext(injector, () => new NotificationsService())
};

View File

@@ -8,6 +8,7 @@ import {
import { Store } from '@ngrx/store';
import type { Message, Room } from '../../../../shared-kernel';
import { NotificationAudioService, AppSound } from '../../../../core/services/notification-audio.service';
import { AppI18nService } from '../../../../core/i18n';
import { TimeSyncService } from '../../../../core/services/time-sync.service';
import { DatabaseService } from '../../../../infrastructure/persistence';
import {
@@ -48,6 +49,7 @@ export class NotificationsService {
private readonly store = inject(Store);
private readonly db = inject(DatabaseService);
private readonly audio = inject(NotificationAudioService);
private readonly appI18n = inject(AppI18nService);
private readonly timeSync = inject(TimeSyncService);
private readonly desktopNotifications = inject(DesktopNotificationService);
private readonly storage = inject(NotificationSettingsStorageService);
@@ -221,7 +223,8 @@ export class NotificationsService {
message,
room,
this._settings(),
!context.isWindowFocused || !context.isDocumentVisible
!context.isWindowFocused || !context.isDocumentVisible,
(key, params) => this.appI18n.instant(key, params)
);
if (this.shouldPlayNotificationSound()) {

View File

@@ -10,6 +10,8 @@ export const DEFAULT_TEXT_CHANNEL_ID = 'general';
const MESSAGE_PREVIEW_LIMIT = 140;
export type AppTranslateFn = (key: string, params?: Record<string, string | number>) => string;
export function resolveMessageChannelId(message: Pick<Message, 'channelId'>): string {
return message.channelId || DEFAULT_TEXT_CHANNEL_ID;
}
@@ -98,17 +100,18 @@ export function buildNotificationDisplayPayload(
message: Pick<Message, 'channelId' | 'content' | 'senderName'>,
room: Room | null,
settings: NotificationsSettings,
requestAttention: boolean
requestAttention: boolean,
translate: AppTranslateFn
): NotificationDisplayPayload {
const channelId = resolveMessageChannelId(message);
const roomName = room?.name || 'Server';
const roomName = room?.name || translate('notifications.display.defaultServerName');
const channelLabel = getChannelLabel(room, channelId);
return {
title: `${roomName} · #${channelLabel}`,
body: settings.showPreview
? formatMessagePreview(message.senderName, message.content)
: `${message.senderName} sent a new message`,
? formatMessagePreview(message.senderName, message.content, translate)
: translate('notifications.display.newMessageHidden', { sender: message.senderName }),
requestAttention
};
}
@@ -147,16 +150,16 @@ export function calculateUnreadForRoom(
};
}
function formatMessagePreview(senderName: string, content: string): string {
function formatMessagePreview(senderName: string, content: string, translate: AppTranslateFn): string {
const normalisedContent = content.replace(/\s+/g, ' ').trim();
if (!normalisedContent) {
return `${senderName} sent a new message`;
return translate('notifications.display.newMessageEmpty', { sender: senderName });
}
const preview = normalisedContent.length > MESSAGE_PREVIEW_LIMIT
? `${normalisedContent.slice(0, MESSAGE_PREVIEW_LIMIT - 1)}...`
: normalisedContent;
return `${senderName}: ${preview}`;
return translate('notifications.display.preview', { sender: senderName, content: preview });
}

View File

@@ -9,10 +9,9 @@
</div>
<div class="min-w-0 flex-1">
<h4 class="text-base font-semibold text-foreground">Delivery</h4>
<h4 class="text-base font-semibold text-foreground">{{ 'notifications.delivery.title' | translate }}</h4>
<p class="mt-1 text-sm text-muted-foreground">
Desktop alerts use the system notification center on Linux and request taskbar attention when the app is not focused. Maximized app window
suppress system popups, and only play the configured notification sound while the app is in the background.
{{ 'notifications.delivery.description' | translate }}
</p>
</div>
</div>
@@ -20,8 +19,8 @@
<div class="mt-5 space-y-4">
<div class="flex items-center justify-between gap-4">
<div>
<p class="font-medium text-foreground">Enable notifications</p>
<p class="text-sm text-muted-foreground">Mute every server and channel notification without affecting unread indicators.</p>
<p class="font-medium text-foreground">{{ 'notifications.enable.label' | translate }}</p>
<p class="text-sm text-muted-foreground">{{ 'notifications.enable.description' | translate }}</p>
</div>
<label class="relative inline-flex cursor-pointer items-center">
@@ -45,8 +44,8 @@
/>
<div>
<p class="font-medium text-foreground">Show message preview</p>
<p class="text-sm text-muted-foreground">Include a short message preview in desktop notifications when content privacy allows it.</p>
<p class="font-medium text-foreground">{{ 'notifications.showPreview.label' | translate }}</p>
<p class="text-sm text-muted-foreground">{{ 'notifications.showPreview.description' | translate }}</p>
</div>
</div>
@@ -71,8 +70,8 @@
/>
<div>
<p class="font-medium text-foreground">Respect busy status</p>
<p class="text-sm text-muted-foreground">Suppress desktop alerts while your user presence is set to busy.</p>
<p class="font-medium text-foreground">{{ 'notifications.respectBusy.label' | translate }}</p>
<p class="text-sm text-muted-foreground">{{ 'notifications.respectBusy.description' | translate }}</p>
</div>
</div>
@@ -101,16 +100,16 @@
</div>
<div class="min-w-0 flex-1">
<h4 class="text-base font-semibold text-foreground">Server Overrides</h4>
<h4 class="text-base font-semibold text-foreground">{{ 'notifications.serverOverrides.title' | translate }}</h4>
<p class="mt-1 text-sm text-muted-foreground">
Right-click actions mirror these switches. Muted servers and channels still collect unread badges so you can catch up later.
{{ 'notifications.serverOverrides.description' | translate }}
</p>
</div>
</div>
@if (rooms().length === 0) {
<div class="mt-5 rounded-lg border border-dashed border-border bg-secondary/20 p-4 text-sm text-muted-foreground">
Join a server to configure notification overrides.
{{ 'notifications.serverOverrides.empty' | translate }}
</div>
} @else {
<div class="mt-5 space-y-4">
@@ -122,11 +121,13 @@
<p class="truncate font-medium text-foreground">{{ room.name }}</p>
@if (roomUnreadCount(room.id) > 0) {
<span class="rounded-full bg-amber-400/20 px-2 py-0.5 text-[11px] font-semibold text-amber-300">
{{ formatUnreadCount(roomUnreadCount(room.id)) }} unread
{{ 'notifications.serverOverrides.unread' | translate: { count: formatUnreadCount(roomUnreadCount(room.id)) } }}
</span>
}
</div>
<p class="mt-1 text-sm text-muted-foreground">{{ room.description || 'Notifications for every text channel in this server.' }}</p>
<p class="mt-1 text-sm text-muted-foreground">
{{ room.description || ('notifications.serverOverrides.defaultRoomDescription' | translate) }}
</p>
</div>
<label class="relative inline-flex cursor-pointer items-center">
@@ -144,8 +145,8 @@
<div class="mt-4 rounded-lg border border-border/70 bg-background/50 p-3">
<div class="mb-2 flex items-center justify-between">
<p class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Channels</p>
<p class="text-xs text-muted-foreground">Unread badges remain visible even if muted.</p>
<p class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">{{ 'notifications.channels.title' | translate }}</p>
<p class="text-xs text-muted-foreground">{{ 'notifications.channels.mutedHint' | translate }}</p>
</div>
<div class="space-y-2">

View File

@@ -16,11 +16,12 @@ import {
import { selectSavedRooms } from '../../../../../store/rooms/rooms.selectors';
import type { Room } from '../../../../../shared-kernel';
import { NotificationsFacade } from '../../../application/facades/notifications.facade';
import { APP_TRANSLATE_IMPORTS } from '../../../../../core/i18n';
@Component({
selector: 'app-notifications-settings',
standalone: true,
imports: [CommonModule, NgIcon],
imports: [CommonModule, NgIcon, ...APP_TRANSLATE_IMPORTS],
viewProviders: [
provideIcons({
lucideBell,