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
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:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user