diff --git a/src/app/admin/admin-item-list/admin-item-list.component.html b/src/app/admin/admin-item-list/admin-item-list.component.html index 08da0c7f4..66f599aa2 100644 --- a/src/app/admin/admin-item-list/admin-item-list.component.html +++ b/src/app/admin/admin-item-list/admin-item-list.component.html @@ -78,6 +78,12 @@ context: { $implicit: { link: 'voice-server', title: 'Voice server' } } " > + +
+ + {{ guildControl.get('name').value }} +
+ +
+
+ Admin notifications channel: + +
+ +
+ Substitute notifications channel: + + + mention role: + +
+ +
+ Queue prompts channel: + +
+
+ diff --git a/src/app/admin/discord/discord-guild-edit/discord-guild-edit.component.scss b/src/app/admin/discord/discord-guild-edit/discord-guild-edit.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/admin/discord/discord-guild-edit/discord-guild-edit.component.ts b/src/app/admin/discord/discord-guild-edit/discord-guild-edit.component.ts new file mode 100644 index 000000000..683a14013 --- /dev/null +++ b/src/app/admin/discord/discord-guild-edit/discord-guild-edit.component.ts @@ -0,0 +1,85 @@ +import { + ChangeDetectionStrategy, + Component, + Input, + OnInit, +} from '@angular/core'; +import { DiscordService } from '../discord.service'; +import { ReplaySubject, distinctUntilChanged, map } from 'rxjs'; +import { TextChannel } from '../models/text-channel'; +import { FormControl, FormGroup } from '@angular/forms'; +import { Role } from '../models/role'; + +@Component({ + selector: 'app-discord-guild-edit', + templateUrl: './discord-guild-edit.component.html', + styleUrls: ['./discord-guild-edit.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DiscordGuildEditComponent implements OnInit { + @Input() + guildControl: FormGroup<{ + id: FormControl; + name: FormControl; + isEnabled: FormControl; + adminNotifications: FormGroup<{ channel: FormControl }>; + substituteNotifications: FormGroup<{ + channel: FormControl; + role: FormControl; + }>; + queuePrompts: FormGroup<{ channel: FormControl }>; + }>; + + textChannels = new ReplaySubject>(1); + roles = new ReplaySubject(1); + + constructor(private readonly discordService: DiscordService) {} + + ngOnInit() { + this.guildControl + .get('isEnabled') + .valueChanges.pipe(distinctUntilChanged()) + .subscribe(isEnabled => { + if (isEnabled) { + this.loadTextChannels(); + this.loadRoles(); + } + }); + + if (this.guildControl.get('isEnabled').value) { + this.loadTextChannels(); + this.loadRoles(); + } + } + + loadTextChannels() { + this.discordService + .fetchTextChannels(this.guildControl.get('id').value) + .pipe( + map(textChannels => + textChannels.reduce((prev, curr) => { + if (!prev.has(curr.parent)) { + prev.set(curr.parent, []); + } + + prev.get(curr.parent).push(curr); + return prev; + }, new Map()), + ), + map(groups => { + groups.forEach(value => + value.sort((a, b) => a.position - b.position), + ); + return groups; + }), + ) + .subscribe(textChannels => this.textChannels.next(textChannels)); + } + + loadRoles() { + this.discordService + .fetchTextChannels(this.guildControl.get('id').value) + .pipe(map(roles => roles.sort((a, b) => a.name.localeCompare(b.name)))) + .subscribe(roles => this.roles.next(roles)); + } +} diff --git a/src/app/admin/discord/discord.component.html b/src/app/admin/discord/discord.component.html new file mode 100644 index 000000000..ef870beab --- /dev/null +++ b/src/app/admin/discord/discord.component.html @@ -0,0 +1,19 @@ +
+ +
+ +
+ +
+
+
+
+
diff --git a/src/app/admin/discord/discord.component.scss b/src/app/admin/discord/discord.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/admin/discord/discord.component.ts b/src/app/admin/discord/discord.component.ts new file mode 100644 index 000000000..739950291 --- /dev/null +++ b/src/app/admin/discord/discord.component.ts @@ -0,0 +1,142 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + OnInit, +} from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { Guild } from './models/guild'; +import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { DiscordConfiguration } from './models/discord-configuration'; +import { Location } from '@angular/common'; +import { ConfigurationService } from '@app/configuration/configuration.service'; +import { TextChannel } from './models/text-channel'; + +@Component({ + selector: 'app-discord', + templateUrl: './discord.component.html', + styleUrls: ['./discord.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DiscordComponent implements OnInit { + private readonly destroyed = new Subject(); + + form = this.formBuilder.group({ + guilds: this.formBuilder.array< + FormGroup<{ + id: FormControl; + name: FormControl; + isEnabled: FormControl; + adminNotifications: FormGroup<{ + channel: FormControl; + }>; + substituteNotifications: FormGroup<{ + channel: FormControl; + role: FormControl; + }>; + queuePrompts: FormGroup<{ channel: FormControl }>; + }> + >([]), + }); + + isSaving = new BehaviorSubject(false); + textChannels = new BehaviorSubject>>( + new Map(), + ); + + constructor( + private readonly route: ActivatedRoute, + private readonly formBuilder: FormBuilder, + private readonly changeDetector: ChangeDetectorRef, + private readonly configurationService: ConfigurationService, + private readonly location: Location, + ) {} + + ngOnInit() { + this.route.data + .pipe(takeUntil(this.destroyed)) + .subscribe( + ({ + configuration, + guilds, + }: { + configuration: DiscordConfiguration; + guilds: Guild[]; + }) => { + this.form.setControl( + 'guilds', + this.formBuilder.array( + guilds.map(guild => { + const config = configuration.guilds.find( + g => g.id === guild.id, + ); + return this.formBuilder.group({ + id: guild.id, + name: guild.name, + isEnabled: Boolean(config), + adminNotifications: this.formBuilder.group({ + channel: config?.adminNotifications?.channel ?? null, + }), + substituteNotifications: this.formBuilder.group({ + channel: config?.substituteNotifications?.channel ?? null, + role: config?.substituteNotifications?.role ?? null, + }), + queuePrompts: this.formBuilder.group({ + channel: config?.queuePrompts?.channel ?? null, + }), + }); + }), + ), + ); + this.changeDetector.markForCheck(); + }, + ); + } + + get guilds() { + return this.form.get('guilds') as FormArray; + } + + save() { + this.isSaving.next(true); + const config: DiscordConfiguration = { + guilds: this.form.value.guilds + .filter(value => value.isEnabled) + .map(value => ({ + id: value.id, + ...(value.adminNotifications.channel + ? { + adminNotifications: { + channel: value.adminNotifications.channel, + }, + } + : {}), + ...(value.substituteNotifications.channel + ? { + substituteNotifications: { + channel: value.substituteNotifications.channel, + role: value.substituteNotifications.role ?? undefined, + }, + } + : {}), + ...(value.queuePrompts.channel + ? { + queuePrompts: { + channel: value.queuePrompts.channel, + bumpPlayerThresholdRatio: 0.5, + }, + } + : {}), + })), + }; + + this.configurationService + .storeValues({ + key: 'discord.guilds', + value: config.guilds, + }) + .subscribe(() => this.location.back()); + } +} diff --git a/src/app/admin/discord/discord.service.ts b/src/app/admin/discord/discord.service.ts new file mode 100644 index 000000000..3ab7c88d1 --- /dev/null +++ b/src/app/admin/discord/discord.service.ts @@ -0,0 +1,33 @@ +import { HttpClient } from '@angular/common/http'; +import { Inject, Injectable } from '@angular/core'; +import { API_URL } from '@app/api-url'; +import { Observable } from 'rxjs'; +import { Guild } from './models/guild'; +import { TextChannel } from './models/text-channel'; +import { Role } from './models/role'; + +@Injectable({ + providedIn: 'root', +}) +export class DiscordService { + constructor( + private readonly http: HttpClient, + @Inject(API_URL) private readonly apiUrl: string, + ) {} + + fetchGuilds(): Observable { + return this.http.get(`${this.apiUrl}/discord/guilds`); + } + + fetchTextChannels(guildId: string): Observable { + return this.http.get( + `${this.apiUrl}/discord/guilds/${guildId}/text-channels`, + ); + } + + fetchRoles(guildId: string): Observable { + return this.http.get( + `${this.apiUrl}/discord/guilds/${guildId}/roles`, + ); + } +} diff --git a/src/app/admin/discord/guilds.resolver.ts b/src/app/admin/discord/guilds.resolver.ts new file mode 100644 index 000000000..59f72dd87 --- /dev/null +++ b/src/app/admin/discord/guilds.resolver.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; +import { Observable } from 'rxjs'; +import { Guild } from './models/guild'; +import { DiscordService } from './discord.service'; + +@Injectable({ + providedIn: 'root', +}) +export class GuildsResolver implements Resolve { + constructor(private readonly discordService: DiscordService) {} + + resolve(): Observable { + return this.discordService.fetchGuilds(); + } +} diff --git a/src/app/admin/discord/models/discord-configuration.ts b/src/app/admin/discord/models/discord-configuration.ts new file mode 100644 index 000000000..dad886ab5 --- /dev/null +++ b/src/app/admin/discord/models/discord-configuration.ts @@ -0,0 +1,5 @@ +import { GuildConfiguration } from './guild-configuration'; + +export interface DiscordConfiguration { + guilds: GuildConfiguration[]; +} diff --git a/src/app/admin/discord/models/guild-configuration.ts b/src/app/admin/discord/models/guild-configuration.ts new file mode 100644 index 000000000..961a9df26 --- /dev/null +++ b/src/app/admin/discord/models/guild-configuration.ts @@ -0,0 +1,13 @@ +export interface GuildConfiguration { + id: string; + substituteNotifications?: { + channel?: string; + role?: string; + }; + queuePrompts?: { + channel?: string; + }; + adminNotifications?: { + channel?: string; + }; +} diff --git a/src/app/admin/discord/models/guild.ts b/src/app/admin/discord/models/guild.ts new file mode 100644 index 000000000..cd9acc612 --- /dev/null +++ b/src/app/admin/discord/models/guild.ts @@ -0,0 +1,4 @@ +export interface Guild { + id: string; + name: string; +} diff --git a/src/app/admin/discord/models/role.ts b/src/app/admin/discord/models/role.ts new file mode 100644 index 000000000..73226e65c --- /dev/null +++ b/src/app/admin/discord/models/role.ts @@ -0,0 +1,5 @@ +export interface Role { + id: string; + name: string; + position: number; +} diff --git a/src/app/admin/discord/models/text-channel.ts b/src/app/admin/discord/models/text-channel.ts new file mode 100644 index 000000000..b3f698ecc --- /dev/null +++ b/src/app/admin/discord/models/text-channel.ts @@ -0,0 +1,6 @@ +export interface TextChannel { + id: string; + name: string; + position: number; + parent?: string; +}