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

@@ -10,8 +10,8 @@
<button
type="button"
class="flex min-w-0 flex-1 items-center gap-3 rounded-md py-1 pr-2 text-left transition-colors hover:bg-secondary/60 focus:outline-none focus:ring-2 focus:ring-primary/50"
[attr.aria-label]="'Open profile for ' + peerName()"
[title]="'Open profile for ' + peerName()"
[attr.aria-label]="openProfileLabel(peerName())"
[title]="openProfileLabel(peerName())"
(click)="openHeaderProfileCard($event)"
>
<app-user-avatar
@@ -23,7 +23,7 @@
/>
<div class="min-w-0 flex-1">
<h1 class="truncate text-base font-semibold text-foreground">{{ peerName() }}</h1>
<p class="text-xs text-muted-foreground">Direct Message</p>
<p class="text-xs text-muted-foreground">{{ 'dm.chat.directMessage' | translate }}</p>
</div>
</button>
} @else {
@@ -36,7 +36,9 @@
/>
<div class="min-w-0 flex-1">
<h1 class="truncate text-base font-semibold text-foreground">{{ peerName() }}</h1>
<p class="text-xs text-muted-foreground">{{ isGroupConversation() ? 'Group Chat' : 'Direct Message' }}</p>
<p class="text-xs text-muted-foreground">
{{ isGroupConversation() ? ('dm.chat.groupChat' | translate) : ('dm.chat.directMessage' | translate) }}
</p>
</div>
}
@if (showCallButton() && conversation()) {
@@ -44,8 +46,8 @@
type="button"
class="grid h-9 w-9 place-items-center rounded-md bg-emerald-500 text-white transition-colors hover:bg-emerald-600 disabled:opacity-50"
[disabled]="!canCallConversation()"
[attr.aria-label]="'Call ' + peerName()"
[title]="'Call ' + peerName()"
[attr.aria-label]="callPeerLabel(peerName())"
[title]="callPeerLabel(peerName())"
(click)="callConversation()"
>
<ng-icon
@@ -101,7 +103,7 @@
data-testid="dm-typing-indicator"
class="px-4 pb-1 text-xs text-muted-foreground"
>
{{ typingUsers().join(', ') }} {{ typingUsers().length === 1 ? 'is' : 'are' }} typing...
{{ typingIndicatorLabel(typingUsers()) }}
</div>
}
@@ -135,7 +137,7 @@
class="fixed inset-0 z-[89]"
tabindex="0"
role="button"
aria-label="Close GIF picker"
[attr.aria-label]="'dm.chat.closeGifPicker' | translate"
(click)="closeGifPicker()"
(keydown.enter)="closeGifPicker()"
(keydown.space)="closeGifPicker()"
@@ -171,6 +173,6 @@
(imageContextMenuRequested)="openImageContextMenu($event)"
/>
} @else {
<div class="flex flex-1 items-center justify-center px-6 text-sm text-muted-foreground">Select a direct message from the rail.</div>
<div class="flex flex-1 items-center justify-center px-6 text-sm text-muted-foreground">{{ 'dm.chat.selectPrompt' | translate }}</div>
}
</section>

View File

@@ -14,6 +14,7 @@ import { ActivatedRoute } from '@angular/router';
import { Store } from '@ngrx/store';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';
import { AppI18nService, APP_TRANSLATE_IMPORTS } from '../../../../core/i18n';
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
import { ViewportService } from '../../../../core/platform';
import {
@@ -71,7 +72,8 @@ interface DmStatusLabel {
BottomSheetComponent,
NgIcon,
ThemeNodeDirective,
UserAvatarComponent
UserAvatarComponent,
...APP_TRANSLATE_IMPORTS
],
viewProviders: [provideIcons({ lucidePhone, lucidePhoneCall })],
templateUrl: './dm-chat.component.html',
@@ -92,6 +94,7 @@ export class DmChatComponent {
private readonly viewport = inject(ViewportService);
private readonly metadataRequestKeys = new Set<string>();
private openedConversationId: string | null = null;
private readonly i18n = inject(AppI18nService);
readonly isMobile = this.viewport.isMobile;
readonly directCalls = inject(DirectCallService);
readonly directMessages = inject(DirectMessageService);
@@ -193,7 +196,7 @@ export class DmChatComponent {
roomId: conversation.id,
channelId: 'direct-message',
senderId: message.senderId,
senderName: knownUser?.displayName || participant?.displayName || (message.senderId === this.currentUserId() ? 'You' : message.senderId),
senderName: knownUser?.displayName || participant?.displayName || (message.senderId === this.currentUserId() ? this.i18n.instant('common.labels.you') : message.senderId),
content: message.content,
timestamp: message.timestamp,
kind: message.kind,
@@ -216,7 +219,7 @@ export class DmChatComponent {
const peerId = conversation?.participants.find((participantId) => participantId !== currentUserId);
return peerId ? conversation?.participantProfiles[peerId]?.displayName || peerId : 'Direct Message';
return peerId ? conversation?.participantProfiles[peerId]?.displayName || peerId : this.i18n.instant('dm.chat.defaultTitle');
});
readonly peerCallIcon = computed(() => {
const conversation = this.conversation();
@@ -382,6 +385,22 @@ export class DmChatComponent {
}
}
openProfileLabel(name: string): string {
return this.i18n.instant('dm.chat.openProfile', { name });
}
callPeerLabel(name: string): string {
return this.i18n.instant('dm.chat.callPeer', { name });
}
typingIndicatorLabel(names: string[]): string {
const verb = names.length === 1
? this.i18n.instant('dm.chat.typingOne')
: this.i18n.instant('dm.chat.typingMany');
return `${names.join(', ')} ${verb}`;
}
closeGifPicker(): void {
this.showGifPicker.set(false);
}

View File

@@ -4,8 +4,8 @@
<button
type="button"
class="relative z-10 flex h-11 w-11 cursor-pointer flex-shrink-0 items-center justify-center border border-transparent text-muted-foreground transition-[border-radius,box-shadow,background-color,color] duration-100 hover:rounded-lg hover:bg-card hover:text-foreground md:h-10 md:w-10"
title="Direct Messages"
aria-label="Direct Messages"
[title]="'dm.rail.title' | translate"
[attr.aria-label]="'dm.rail.ariaLabel' | translate"
[ngClass]="isOnDirectMessages() ? 'rounded-lg ring-2 ring-primary/40 bg-primary/10 text-foreground' : 'rounded-xl bg-card'"
[attr.aria-current]="isOnDirectMessages() ? 'page' : null"
(click)="openDirectMessages()"

View File

@@ -20,6 +20,7 @@ import {
lucideUsers
} from '@ng-icons/lucide';
import { filter, map } from 'rxjs';
import { AppI18nService, APP_TRANSLATE_IMPORTS } from '../../../../core/i18n';
import { ContextMenuComponent } from '../../../../shared';
import { DirectMessageService } from '../../application/services/direct-message.service';
import { FriendService } from '../../application/services/friend.service';
@@ -51,7 +52,8 @@ const EXIT_ANIMATION_MS = 160;
imports: [
CommonModule,
ContextMenuComponent,
NgIcon
NgIcon,
...APP_TRANSLATE_IMPORTS
],
viewProviders: [provideIcons({ lucideLogOut, lucideMessageCircle, lucideTrash2, lucideUser, lucideUsers })],
templateUrl: './dm-rail.component.html',
@@ -61,6 +63,7 @@ export class DmRailComponent implements OnDestroy {
private readonly router = inject(Router);
private readonly store = inject(Store);
private readonly exitTimers = new Map<string, ReturnType<typeof setTimeout>>();
private readonly i18n = inject(AppI18nService);
readonly directMessages = inject(DirectMessageService);
readonly friends = inject(FriendService);
readonly users = this.store.selectSignal(selectAllUsers);
@@ -198,7 +201,7 @@ export class DmRailComponent implements OnDestroy {
const peerId = conversation.participants.find((participantId) => participantId !== this.currentUserId());
return peerId ? conversation.participantProfiles[peerId]?.displayName || peerId : 'DM';
return peerId ? conversation.participantProfiles[peerId]?.displayName || peerId : this.i18n.instant('dm.chat.railFallback');
}
initial(label: string): string {
@@ -263,7 +266,9 @@ export class DmRailComponent implements OnDestroy {
}
forgetContextLabel(item: DmRailItem): string {
return item.conversation && this.isGroupConversation(item.conversation) ? 'Leave chat' : 'Forget chat';
return item.conversation && this.isGroupConversation(item.conversation)
? this.i18n.instant('dm.rail.leaveChat')
: this.i18n.instant('dm.rail.forgetChat');
}
forgetContextIcon(item: DmRailItem): string {

View File

@@ -30,8 +30,8 @@
type="button"
class="invisible grid h-7 w-7 shrink-0 place-items-center rounded-md text-muted-foreground opacity-0 transition hover:bg-emerald-500/10 hover:text-emerald-600 focus:visible focus:opacity-100 group-focus-within:visible group-focus-within:opacity-100 group-hover:visible group-hover:opacity-100 disabled:group-focus-within:opacity-30 disabled:group-hover:opacity-30"
[disabled]="!canCall()"
[attr.aria-label]="'Call ' + peerName()"
[title]="'Call ' + peerName()"
[attr.aria-label]="callPeerLabel(peerName())"
[title]="callPeerLabel(peerName())"
(click)="callConversationPeer($event)"
>
<ng-icon
@@ -42,8 +42,8 @@
<button
type="button"
class="grid h-7 w-7 shrink-0 place-items-center rounded-md text-muted-foreground opacity-0 transition hover:bg-destructive/10 hover:text-destructive focus:opacity-100 group-hover:opacity-100"
[attr.aria-label]="'Forget ' + peerName()"
[title]="'Forget ' + peerName()"
[attr.aria-label]="forgetPeerLabel(peerName())"
[title]="forgetPeerLabel(peerName())"
(click)="forgetConversation($event)"
>
<ng-icon

View File

@@ -18,6 +18,7 @@ import {
lucideTrash2
} from '@ng-icons/lucide';
import { map } from 'rxjs';
import { AppI18nService, APP_TRANSLATE_IMPORTS } from '../../../../core/i18n';
import { UserAvatarComponent } from '../../../../shared';
import { ThemeNodeDirective } from '../../../theme';
import { AttachmentFacade } from '../../../attachment';
@@ -35,7 +36,8 @@ import { DirectMessageService } from '../../application/services/direct-message.
CommonModule,
NgIcon,
UserAvatarComponent,
ThemeNodeDirective
ThemeNodeDirective,
...APP_TRANSLATE_IMPORTS
],
viewProviders: [provideIcons({ lucidePhone, lucidePhoneCall, lucideTrash2 })],
host: { class: 'block' },
@@ -48,6 +50,7 @@ export class DmConversationItemComponent {
private readonly attachments = inject(AttachmentFacade);
private readonly directMessages = inject(DirectMessageService);
private readonly directCalls = inject(DirectCallService);
private readonly i18n = inject(AppI18nService);
readonly conversation = input.required<DirectMessageConversation>();
readonly conversationOpened = output<string>();
readonly users = this.store.selectSignal(selectAllUsers);
@@ -95,6 +98,14 @@ export class DmConversationItemComponent {
await this.directCalls.startConversationCall(this.conversation());
}
callPeerLabel(name: string): string {
return this.i18n.instant('dm.chat.callPeer', { name });
}
forgetPeerLabel(name: string): string {
return this.i18n.instant('dm.chat.forgetPeer', { name });
}
formatUnreadCount(count: number): string {
return count > 99 ? '99+' : String(count);
}
@@ -107,7 +118,7 @@ export class DmConversationItemComponent {
const peerId = this.peerId(conversation);
const knownUser = this.peerUser(conversation);
return peerId ? knownUser?.displayName || conversation.participantProfiles[peerId]?.displayName || peerId : 'Direct Message';
return peerId ? knownUser?.displayName || conversation.participantProfiles[peerId]?.displayName || peerId : this.i18n.instant('dm.chat.defaultTitle');
}
private resolvePeerAvatarUrl(conversation: DirectMessageConversation): string | undefined {
@@ -125,15 +136,15 @@ export class DmConversationItemComponent {
const lastMessage = conversation.messages.at(-1);
if (!lastMessage) {
return 'No messages yet';
return this.i18n.instant('dm.previews.noMessages');
}
if (lastMessage.isDeleted) {
return 'Message deleted';
return this.i18n.instant('dm.previews.deleted');
}
if (this.isKlipyGif(lastMessage.content)) {
return 'Sent a GIF';
return this.i18n.instant('dm.previews.gif');
}
this.attachments.updated();
@@ -143,7 +154,7 @@ export class DmConversationItemComponent {
return this.attachmentPreview(attachments);
}
return lastMessage.content || 'Attachment';
return lastMessage.content || this.i18n.instant('dm.previews.attachment');
}
private conversationCallIcon(conversation: DirectMessageConversation): string {
@@ -203,17 +214,19 @@ export class DmConversationItemComponent {
private attachmentPreview(attachments: Attachment[]): string {
if (attachments.some((attachment) => attachment.mime.startsWith('image/'))) {
return 'Sent an image';
return this.i18n.instant('dm.previews.image');
}
if (attachments.some((attachment) => attachment.mime.startsWith('video/'))) {
return 'Sent a video';
return this.i18n.instant('dm.previews.video');
}
if (attachments.some((attachment) => attachment.mime.startsWith('audio/'))) {
return 'Sent audio';
return this.i18n.instant('dm.previews.audio');
}
return attachments.length === 1 ? 'Sent an attachment' : 'Sent attachments';
return attachments.length === 1
? this.i18n.instant('dm.previews.oneAttachment')
: this.i18n.instant('dm.previews.manyAttachments');
}
}

View File

@@ -15,8 +15,8 @@
/>
</div>
<div class="min-w-0">
<h1 class="truncate text-sm font-semibold text-foreground">Direct Messages</h1>
<p class="text-xs text-muted-foreground">{{ directMessages.conversations().length }} chats</p>
<h1 class="truncate text-sm font-semibold text-foreground">{{ 'dm.conversations.title' | translate }}</h1>
<p class="text-xs text-muted-foreground">{{ 'dm.conversations.chatCount' | translate: { count: directMessages.conversations().length } }}</p>
</div>
</header>
@@ -25,7 +25,9 @@
class="flex min-h-0 flex-1 flex-col"
>
@if (directMessages.conversations().length === 0) {
<div class="flex h-full items-center justify-center px-4 text-center text-sm text-muted-foreground">No direct messages yet.</div>
<div class="flex h-full items-center justify-center px-4 text-center text-sm text-muted-foreground">
{{ 'dm.conversations.empty' | translate }}
</div>
} @else {
<app-virtual-list
class="block h-full p-2"

View File

@@ -8,6 +8,7 @@ import {
import { CommonModule } from '@angular/common';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideMessageCircle } from '@ng-icons/lucide';
import { APP_TRANSLATE_IMPORTS } from '../../../../core/i18n';
import { ThemeNodeDirective, ThemeService } from '../../../theme';
import { VoiceControlsComponent } from '../../../voice-session';
import { VirtualListComponent } from '../../../../shared/components/virtual-list';
@@ -24,7 +25,8 @@ import { DmConversationItemComponent } from './dm-conversation-item.component';
NgIcon,
ThemeNodeDirective,
VirtualListComponent,
VoiceControlsComponent
VoiceControlsComponent,
...APP_TRANSLATE_IMPORTS
],
viewProviders: [provideIcons({ lucideMessageCircle })],
host: { class: 'contents' },

View File

@@ -28,21 +28,21 @@
type="button"
(click)="setMobilePage('conversations')"
class="grid h-11 w-11 place-items-center rounded-lg text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
aria-label="Back to conversations"
[attr.aria-label]="'dm.workspace.backToConversations' | translate"
>
<ng-icon
name="lucideChevronLeft"
class="h-5 w-5"
/>
</button>
<p class="min-w-0 flex-1 truncate text-sm font-semibold text-foreground">Direct messages</p>
<p class="min-w-0 flex-1 truncate text-sm font-semibold text-foreground">{{ 'dm.workspace.directMessages' | translate }}</p>
@if (activeCall()) {
<button
type="button"
(click)="openActiveCall()"
class="grid h-11 w-11 place-items-center rounded-lg text-emerald-600 transition-colors hover:bg-emerald-500/10 hover:text-emerald-500"
aria-label="Return to call"
title="Return to call"
[attr.aria-label]="'dm.workspace.returnToCall' | translate"
[title]="'dm.workspace.returnToCall' | translate"
>
<ng-icon
name="lucidePhoneCall"

View File

@@ -17,6 +17,7 @@ import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideChevronLeft, lucidePhoneCall } from '@ng-icons/lucide';
import { APP_TRANSLATE_IMPORTS } from '../../../../core/i18n';
import { ServersRailComponent } from '../../../../features/servers/servers-rail/servers-rail.component';
import { ViewportService } from '../../../../core/platform';
import { ThemeService } from '../../../theme';
@@ -46,7 +47,8 @@ interface SwiperElement extends HTMLElement {
NgIcon,
DmChatPanelComponent,
DmConversationsPanelComponent,
ServersRailComponent
ServersRailComponent,
...APP_TRANSLATE_IMPORTS
],
viewProviders: [provideIcons({ lucideChevronLeft, lucidePhoneCall })],
schemas: [CUSTOM_ELEMENTS_SCHEMA],

View File

@@ -3,7 +3,7 @@
<header class="flex items-center gap-3 border-b border-border px-4 py-3">
<a
routerLink="/dashboard"
aria-label="Back to dashboard"
[attr.aria-label]="'common.actions.backToDashboard' | translate"
class="grid h-9 w-9 shrink-0 place-items-center rounded-lg border border-border bg-secondary text-muted-foreground transition-colors hover:bg-secondary/80"
>
<ng-icon
@@ -12,8 +12,8 @@
/>
</a>
<div class="min-w-0">
<h1 class="truncate text-lg font-semibold text-foreground">Find people</h1>
<p class="truncate text-xs text-muted-foreground">Search for people you share servers with.</p>
<h1 class="truncate text-lg font-semibold text-foreground">{{ 'dm.find.title' | translate }}</h1>
<p class="truncate text-xs text-muted-foreground">{{ 'dm.find.subtitle' | translate }}</p>
</div>
</header>
@@ -25,9 +25,9 @@
/>
<input
type="text"
aria-label="Search people"
[attr.aria-label]="'dm.find.searchAriaLabel' | translate"
class="h-10 w-full rounded-lg border border-border bg-secondary py-2 pl-10 pr-3 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="Search people..."
[placeholder]="'dm.find.searchPlaceholder' | translate"
[ngModel]="searchQuery()"
(ngModelChange)="onSearchChange($event)"
/>
@@ -47,13 +47,13 @@
class="h-7 w-7 text-muted-foreground"
/>
</div>
<p class="text-base font-semibold text-foreground">No people to show yet</p>
<p class="mt-1 max-w-sm text-sm">Join servers to discover people with shared interests.</p>
<p class="text-base font-semibold text-foreground">{{ 'dm.find.emptyTitle' | translate }}</p>
<p class="mt-1 max-w-sm text-sm">{{ 'dm.find.emptyMessage' | translate }}</p>
<a
routerLink="/servers"
class="mt-5 inline-flex items-center justify-center rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90"
>
Find servers
{{ 'dm.find.findServers' | translate }}
</a>
</div>
}

View File

@@ -16,6 +16,7 @@ import {
lucideUsers
} from '@ng-icons/lucide';
import { AppI18nService, APP_TRANSLATE_IMPORTS } from '../../../../core/i18n';
import { UserSearchListComponent } from '../user-search-list/user-search-list.component';
import { selectAllUsers } from '../../../../store/users/users.selectors';
import { selectSavedRooms } from '../../../../store/rooms/rooms.selectors';
@@ -33,7 +34,8 @@ import { selectSavedRooms } from '../../../../store/rooms/rooms.selectors';
FormsModule,
RouterLink,
NgIcon,
UserSearchListComponent
UserSearchListComponent,
...APP_TRANSLATE_IMPORTS
],
viewProviders: [provideIcons({ lucideArrowLeft, lucideSearch, lucideUsers })],
templateUrl: './find-people.component.html',

View File

@@ -3,8 +3,8 @@
[attr.data-testid]="'friend-button-' + userId()"
class="grid h-8 w-8 place-items-center rounded-md border border-border bg-secondary text-foreground transition-colors hover:bg-secondary/80"
[attr.aria-pressed]="isFriend()"
[attr.aria-label]="isFriend() ? 'Remove friend' : 'Add friend'"
[title]="isFriend() ? 'Remove friend' : 'Add friend'"
[attr.aria-label]="ariaLabel()"
[title]="ariaLabel()"
(click)="toggle($event)"
>
<ng-icon

View File

@@ -8,22 +8,29 @@ import {
import { CommonModule } from '@angular/common';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideUserCheck, lucideUserPlus } from '@ng-icons/lucide';
import { AppI18nService, APP_TRANSLATE_IMPORTS } from '../../../../core/i18n';
import { FriendService } from '../../application/services/friend.service';
import type { User } from '../../../../shared-kernel';
@Component({
selector: 'app-friend-button',
standalone: true,
imports: [CommonModule, NgIcon],
imports: [CommonModule, NgIcon, ...APP_TRANSLATE_IMPORTS],
viewProviders: [provideIcons({ lucideUserCheck, lucideUserPlus })],
templateUrl: './friend-button.component.html'
})
export class FriendButtonComponent {
private readonly friends = inject(FriendService);
private readonly i18n = inject(AppI18nService);
readonly user = input.required<User>();
readonly userId = computed(() => this.user().oderId || this.user().id);
readonly isFriend = computed(() => this.friends.isFriend(this.userId()));
readonly ariaLabel = computed(() =>
this.isFriend()
? this.i18n.instant('dm.friend.remove')
: this.i18n.instant('dm.friend.add')
);
toggle(event: Event): void {
event.stopPropagation();

View File

@@ -1,13 +1,13 @@
<section class="min-h-full p-3">
<div class="mb-2 flex items-center justify-between gap-3">
<h3 class="text-sm font-semibold text-foreground">People</h3>
<h3 class="text-sm font-semibold text-foreground">{{ 'dm.search.peopleTitle' | translate }}</h3>
<span class="text-xs text-muted-foreground">{{ matchingUsers().length }}</span>
</div>
@if (friendResults().length > 0) {
<div class="mb-3">
<div class="mb-1 flex items-center justify-between">
<h4 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Friends</h4>
<h4 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">{{ 'dm.search.friendsTitle' | translate }}</h4>
<span class="text-xs text-muted-foreground">{{ friendResults().length }}</span>
</div>
@@ -38,8 +38,8 @@
type="button"
[attr.data-testid]="'call-friend-' + userKey(user)"
class="grid h-8 w-8 place-items-center rounded-md bg-emerald-500 text-white transition-colors hover:bg-emerald-600"
[attr.aria-label]="'Call ' + user.displayName"
[title]="'Call ' + user.displayName"
[attr.aria-label]="callUserLabel(user.displayName)"
[title]="callUserLabel(user.displayName)"
(click)="callUser(user)"
>
<ng-icon
@@ -51,8 +51,8 @@
type="button"
[attr.data-testid]="'message-friend-' + userKey(user)"
class="grid h-8 w-8 place-items-center rounded-md bg-primary text-primary-foreground transition-colors hover:bg-primary/90"
[attr.aria-label]="'Message ' + user.displayName"
[title]="'Message ' + user.displayName"
[attr.aria-label]="messageUserLabel(user.displayName)"
[title]="messageUserLabel(user.displayName)"
(click)="messageUser(user)"
>
<ng-icon
@@ -69,7 +69,7 @@
@if (friendResults().length > 0) {
<div class="mb-1 flex items-center justify-between">
<h4 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Others</h4>
<h4 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">{{ 'dm.search.othersTitle' | translate }}</h4>
<span class="text-xs text-muted-foreground">{{ results().length }}</span>
</div>
}
@@ -80,7 +80,7 @@
name="lucideSearch"
class="h-4 w-4"
/>
No users found
{{ 'dm.search.noUsersFound' | translate }}
</div>
} @else {
<div class="space-y-1.5">
@@ -115,8 +115,8 @@
type="button"
[attr.data-testid]="'call-user-' + userKey(user)"
class="grid h-8 w-8 place-items-center rounded-md bg-emerald-500 text-white transition-colors hover:bg-emerald-600"
[attr.aria-label]="'Call ' + user.displayName"
[title]="'Call ' + user.displayName"
[attr.aria-label]="callUserLabel(user.displayName)"
[title]="callUserLabel(user.displayName)"
(click)="callUser(user)"
>
<ng-icon
@@ -128,8 +128,8 @@
type="button"
[attr.data-testid]="'message-user-' + userKey(user)"
class="grid h-8 w-8 place-items-center rounded-md bg-primary text-primary-foreground transition-colors hover:bg-primary/90"
[attr.aria-label]="'Message ' + user.displayName"
[title]="'Message ' + user.displayName"
[attr.aria-label]="messageUserLabel(user.displayName)"
[title]="messageUserLabel(user.displayName)"
(click)="messageUser(user)"
>
<ng-icon

View File

@@ -15,6 +15,7 @@ import {
lucidePhoneCall,
lucideSearch
} from '@ng-icons/lucide';
import { AppI18nService, APP_TRANSLATE_IMPORTS } from '../../../../core/i18n';
import { selectAllUsers, selectCurrentUser } from '../../../../store/users/users.selectors';
import { selectSavedRooms } from '../../../../store/rooms/rooms.selectors';
import { UserAvatarComponent } from '../../../../shared';
@@ -31,7 +32,8 @@ import type { User } from '../../../../shared-kernel';
CommonModule,
NgIcon,
UserAvatarComponent,
FriendButtonComponent
FriendButtonComponent,
...APP_TRANSLATE_IMPORTS
],
viewProviders: [provideIcons({ lucideMessageCircle, lucidePhone, lucidePhoneCall, lucideSearch })],
templateUrl: './user-search-list.component.html'
@@ -42,6 +44,7 @@ export class UserSearchListComponent {
private readonly directMessages = inject(DirectMessageService);
readonly directCalls = inject(DirectCallService);
readonly friends = inject(FriendService);
private readonly i18n = inject(AppI18nService);
readonly searchQuery = input('');
readonly users = this.store.selectSignal(selectAllUsers);
readonly savedRooms = this.store.selectSignal(selectSavedRooms);
@@ -108,6 +111,14 @@ export class UserSearchListComponent {
return this.directCalls.isCallingUser(user) ? 'lucidePhoneCall' : 'lucidePhone';
}
callUserLabel(name: string): string {
return this.i18n.instant('dm.search.callUser', { name });
}
messageUserLabel(name: string): string {
return this.i18n.instant('dm.search.messageUser', { name });
}
userKey(user: User): string {
return user.oderId || user.id;
}