feat: Allow admin to create new text channels
This commit is contained in:
@@ -23,12 +23,24 @@
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<!-- Chat Area -->
|
||||
<main class="relative flex-1 min-w-0">
|
||||
<div
|
||||
class="h-full overflow-hidden"
|
||||
[class.hidden]="isVoiceWorkspaceExpanded()"
|
||||
>
|
||||
<app-chat-messages />
|
||||
</div>
|
||||
@if (!isVoiceWorkspaceExpanded()) {
|
||||
@if (hasTextChannels()) {
|
||||
<div class="h-full overflow-hidden">
|
||||
<app-chat-messages />
|
||||
</div>
|
||||
} @else {
|
||||
<div class="flex h-full items-center justify-center px-6">
|
||||
<div class="max-w-md text-center text-muted-foreground">
|
||||
<ng-icon
|
||||
name="lucideHash"
|
||||
class="mx-auto mb-4 h-16 w-16 opacity-30"
|
||||
/>
|
||||
<h2 class="mb-2 text-xl font-medium text-foreground">No text channels</h2>
|
||||
<p class="text-sm">There are no existing text channels currently.</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<app-screen-share-workspace />
|
||||
</main>
|
||||
|
||||
@@ -72,10 +72,17 @@ export class ChatRoomComponent {
|
||||
textChannels = this.store.selectSignal(selectTextChannels);
|
||||
voiceChannels = this.store.selectSignal(selectVoiceChannels);
|
||||
isVoiceWorkspaceExpanded = this.voiceWorkspace.isExpanded;
|
||||
hasTextChannels = computed(() => this.textChannels().length > 0);
|
||||
|
||||
activeTextChannelName = computed(() => {
|
||||
const textChannels = this.textChannels();
|
||||
|
||||
if (textChannels.length === 0) {
|
||||
return 'No text channels';
|
||||
}
|
||||
|
||||
const id = this.activeChannelId();
|
||||
const activeChannel = this.textChannels().find((channel) => channel.id === id);
|
||||
const activeChannel = textChannels.find((channel) => channel.id === id) ?? textChannels[0];
|
||||
|
||||
return activeChannel ? activeChannel.name : id;
|
||||
});
|
||||
|
||||
@@ -77,9 +77,12 @@
|
||||
#renameInput
|
||||
type="text"
|
||||
[value]="ch.name"
|
||||
[class.border-destructive]="renamingChannelId() === ch.id && !!channelNameError()"
|
||||
[title]="renamingChannelId() === ch.id ? (channelNameError() ?? '') : ''"
|
||||
(keydown.enter)="confirmRename($event)"
|
||||
(keydown.escape)="cancelRename()"
|
||||
(blur)="confirmRename($event)"
|
||||
(input)="clearChannelNameError()"
|
||||
class="flex-1 bg-secondary border border-border rounded px-1 py-0.5 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
(click)="$event.stopPropagation()"
|
||||
/>
|
||||
@@ -132,9 +135,12 @@
|
||||
#renameInput
|
||||
type="text"
|
||||
[value]="ch.name"
|
||||
[class.border-destructive]="renamingChannelId() === ch.id && !!channelNameError()"
|
||||
[title]="renamingChannelId() === ch.id ? (channelNameError() ?? '') : ''"
|
||||
(keydown.enter)="confirmRename($event)"
|
||||
(keydown.escape)="cancelRename()"
|
||||
(blur)="confirmRename($event)"
|
||||
(input)="clearChannelNameError()"
|
||||
class="flex-1 bg-secondary border border-border rounded px-1 py-0.5 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
(click)="$event.stopPropagation()"
|
||||
/>
|
||||
@@ -483,7 +489,12 @@
|
||||
[(ngModel)]="newChannelName"
|
||||
placeholder="Channel name"
|
||||
class="w-full px-3 py-2 bg-secondary rounded border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary text-sm"
|
||||
[class.border-destructive]="!!channelNameError()"
|
||||
(ngModelChange)="clearChannelNameError()"
|
||||
(keydown.enter)="confirmCreateChannel()"
|
||||
/>
|
||||
@if (channelNameError()) {
|
||||
<p class="mt-2 text-sm text-destructive">{{ channelNameError() }}</p>
|
||||
}
|
||||
</app-confirm-dialog>
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ import { VoiceActivityService, VoiceConnectionFacade } from '../../../domains/vo
|
||||
import { VoiceSessionFacade, VoiceWorkspaceService } from '../../../domains/voice-session';
|
||||
import { VoicePlaybackService } from '../../../domains/voice-connection/application/voice-playback.service';
|
||||
import { VoiceControlsComponent } from '../../../domains/voice-session/feature/voice-controls/voice-controls.component';
|
||||
import {
|
||||
isChannelNameTaken,
|
||||
normalizeChannelName
|
||||
} from '../../../store/rooms/room-channels.rules';
|
||||
import {
|
||||
ContextMenuComponent,
|
||||
UserAvatarComponent,
|
||||
@@ -152,6 +156,7 @@ export class RoomsSidePanelComponent {
|
||||
contextChannel = signal<Channel | null>(null);
|
||||
|
||||
renamingChannelId = signal<string | null>(null);
|
||||
channelNameError = signal<string | null>(null);
|
||||
|
||||
showCreateChannelDialog = signal(false);
|
||||
createChannelType = signal<'text' | 'voice'>('text');
|
||||
@@ -243,6 +248,7 @@ export class RoomsSidePanelComponent {
|
||||
const ch = this.contextChannel();
|
||||
|
||||
this.closeChannelMenu();
|
||||
this.channelNameError.set(null);
|
||||
|
||||
if (ch) {
|
||||
this.renamingChannelId.set(ch.id);
|
||||
@@ -251,10 +257,29 @@ export class RoomsSidePanelComponent {
|
||||
|
||||
confirmRename(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const name = input.value.trim();
|
||||
const name = normalizeChannelName(input.value);
|
||||
const channelId = this.renamingChannelId();
|
||||
|
||||
if (channelId && name) {
|
||||
if (!channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validationError = this.getChannelNameError(name, channelId);
|
||||
|
||||
if (validationError) {
|
||||
this.channelNameError.set(validationError);
|
||||
requestAnimationFrame(() => {
|
||||
input.focus();
|
||||
input.select();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.channelNameError.set(null);
|
||||
|
||||
const currentName = this.currentRoom()?.channels?.find((channel) => channel.id === channelId)?.name;
|
||||
|
||||
if (currentName !== name) {
|
||||
this.store.dispatch(RoomsActions.renameChannel({ channelId, name }));
|
||||
}
|
||||
|
||||
@@ -262,6 +287,7 @@ export class RoomsSidePanelComponent {
|
||||
}
|
||||
|
||||
cancelRename() {
|
||||
this.channelNameError.set(null);
|
||||
this.renamingChannelId.set(null);
|
||||
}
|
||||
|
||||
@@ -300,14 +326,19 @@ export class RoomsSidePanelComponent {
|
||||
createChannel(type: 'text' | 'voice') {
|
||||
this.createChannelType.set(type);
|
||||
this.newChannelName = '';
|
||||
this.channelNameError.set(null);
|
||||
this.showCreateChannelDialog.set(true);
|
||||
}
|
||||
|
||||
confirmCreateChannel() {
|
||||
const name = this.newChannelName.trim();
|
||||
const name = normalizeChannelName(this.newChannelName);
|
||||
|
||||
if (!name)
|
||||
const validationError = this.getChannelNameError(name);
|
||||
|
||||
if (validationError) {
|
||||
this.channelNameError.set(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
const type = this.createChannelType();
|
||||
const existing = type === 'text' ? this.textChannels() : this.voiceChannels();
|
||||
@@ -319,13 +350,35 @@ export class RoomsSidePanelComponent {
|
||||
};
|
||||
|
||||
this.store.dispatch(RoomsActions.addChannel({ channel }));
|
||||
this.channelNameError.set(null);
|
||||
this.showCreateChannelDialog.set(false);
|
||||
}
|
||||
|
||||
cancelCreateChannel() {
|
||||
this.channelNameError.set(null);
|
||||
this.showCreateChannelDialog.set(false);
|
||||
}
|
||||
|
||||
clearChannelNameError(): void {
|
||||
if (this.channelNameError()) {
|
||||
this.channelNameError.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
private getChannelNameError(name: string, excludeChannelId?: string): string | null {
|
||||
if (!name) {
|
||||
return 'Channel name is required.';
|
||||
}
|
||||
|
||||
const channels = this.currentRoom()?.channels ?? [];
|
||||
|
||||
if (isChannelNameTaken(channels, name, excludeChannelId)) {
|
||||
return 'Channel names must be unique in a server.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
openUserContextMenu(evt: MouseEvent, user: User) {
|
||||
evt.preventDefault();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user