Added user management for groups. Improved user experience slightly. Added some error checks for request acks.

This commit is contained in:
Tom 2025-03-20 12:33:27 +00:00
parent 2f2215b041
commit 1acda7978e
40 changed files with 623 additions and 52 deletions

View File

@ -108,7 +108,8 @@
}
</mat-card-content>
<mat-card-actions class="actions">
<mat-card-actions class="actions"
align="end">
@if (!isNew) {
<button mat-raised-button
class="delete"

View File

@ -213,9 +213,9 @@ export class ActionItemEditComponent implements OnInit {
ngOnInit(): void {
this.isNew = this.action.name.length <= 0;
this.previousName = this.action.name;
if (!this.isNew)
if (!this.isNew) {
this.formGroup.get('name')!.disable()
else {
} else {
this.formGroup.get('name')?.addValidators(createItemExistsInArrayValidator(this.actions, a => a.name));
}
}
@ -241,7 +241,13 @@ export class ActionItemEditComponent implements OnInit {
this.waitForResponse = true;
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'delete_redeemable_action' && d.d.request.data.name == this.action.name)
.subscribe({
next: () => this.dialogRef.close(),
next: (d) => {
if (d.d.error) {
// TODO: update & show response error message.
} else {
this.dialogRef.close();
}
},
error: () => this.waitForResponse = false,
complete: () => this.waitForResponse = false,
});
@ -277,7 +283,13 @@ export class ActionItemEditComponent implements OnInit {
const requestType = isNewAction ? 'create_redeemable_action' : 'update_redeemable_action';
this.client.first((d: any) => d.op == 4 && d.d.request.type == requestType && d.d.data.name == this.action.name)
.subscribe({
next: () => this.dialogRef.close(this.action),
next: (d) => {
if (d.d.error) {
// TODO: update & show response error message.
} else {
this.dialogRef.close(this.action);
}
},
error: () => this.waitForResponse = false,
complete: () => this.waitForResponse = false,
});

View File

@ -5,8 +5,7 @@ import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatButtonModule } from '@angular/material/button';
import EventService from '../../shared/services/EventService';
import { HttpClient } from '@angular/common/http';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { first, Subscription, timeout } from 'rxjs';
import { HermesClientService } from '../../hermes-client.service';
import { MatCardModule } from '@angular/material/card';
@ -20,16 +19,16 @@ import { ApiKeyService } from '../../shared/services/api/api-key.service';
styleUrl: './tts-login.component.scss'
})
export class TtsLoginComponent implements OnInit, OnDestroy {
keyService = inject(ApiKeyService);
route = inject(ActivatedRoute);
private readonly client = inject(HermesClientService);
private readonly keyService = inject(ApiKeyService);
private readonly events = inject(EventService);
private readonly route = inject(ActivatedRoute);
api_keys: { id: string, label: string }[] = [];
selected_api_key: string | undefined;
private subscriptions: Subscription[] = [];
constructor(private hermes: HermesClientService, private events: EventService, private http: HttpClient, private router: Router) {
}
ngOnInit(): void {
this.route.data.subscribe(d => this.api_keys = d['keys']);
@ -50,10 +49,10 @@ export class TtsLoginComponent implements OnInit, OnDestroy {
this.subscriptions.forEach(s => s.unsubscribe());
}
login() {
login(): void {
if (!this.selected_api_key)
return;
this.hermes.login(this.selected_api_key);
this.client.login(this.selected_api_key);
}
}

View File

@ -8,7 +8,8 @@
<mat-card-content>
<impersonation />
</mat-card-content>
<mat-card-actions class="actions">
<mat-card-actions class="actions"
align="end">
<div>
@if (isTTSLoggedIn) {
<button mat-raised-button

View File

@ -38,7 +38,7 @@
}
</mat-form-field>
</mat-card-content>
<mat-card-actions>
<mat-card-actions align="end">
<button mat-button
[disabled]="waitForResponse || formGroup.invalid"
(click)="add()">

View File

@ -55,7 +55,13 @@ export class GroupItemEditComponent implements OnInit {
this.waitForResponse = true;
this._client.first((d: any) => d.op == 4 && d.d.request.type == 'create_group' && d.d.data.name == this.nameForm.value)
.subscribe({
next: (d) => this._dialogRef.close(d.d.data),
next: (d) => {
if (d.d.error) {
// TODO: update & show response error message.
} else {
this._dialogRef.close(d.d.data);
}
},
error: () => this.waitForResponse = false,
complete: () => this.waitForResponse = false,
});

View File

@ -4,10 +4,17 @@
<small class="tag">auto-generated</small>
}
</section>
<section class="">{{item().group.priority}}</section>
<section class="">
{{item().group.priority}}
<small class="muted block">priority</small>
</section>
<section>
@if (special) {
<p class="muted">Unknown</p>
} @else {
{{item().chatters.length}}
<small class="muted block">user{{item().chatters.length == 1 ? '' : 's'}}</small>
}
</section>
<section>
{{item().policies.length}}

View File

@ -1,13 +1,12 @@
import { Component, inject, input, Input, OnInit } from '@angular/core';
import { Component, inject, input, OnInit } from '@angular/core';
import { Group } from '../../shared/models/group';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { Policy } from '../../shared/models/policy';
import { GroupItemEditComponent } from '../group-item-edit/group-item-edit.component';
import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { GroupChatter } from '../../shared/models/group-chatter';
import { SpecialGroups } from '../../shared/utils/groups';
@Component({
selector: 'group-item',
@ -24,12 +23,10 @@ export class GroupItemComponent implements OnInit {
readonly router = inject(Router);
item = input.required<{ group: Group, chatters: GroupChatter[], policies: Policy[] }>();
link: string = '';
special: boolean = true;
ngOnInit() {
this.special = ['everyone', 'subscribers', 'moderators', 'vip', 'broadcaster'].includes(this.item().group.name);
this.special = SpecialGroups.includes(this.item().group.name);
this.link = 'groups/' + this.item().group.id;
}
}

View File

@ -1,6 +1,19 @@
<div>
<h2>{{group?.name}}</h2>
@if (!isSpecialGroup) {
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Users</mat-panel-title>
<mat-panel-description class="muted">
{{chatters.length}} user{{chatters.length == 1 ? '' : 's'}}
</mat-panel-description>
</mat-expansion-panel-header>
<twitch-user-list [twitchUsers]="chatters"
[group]="group" />
</mat-expansion-panel>
}
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Policies</mat-panel-title>
@ -12,9 +25,7 @@
[groups]="groups"
[policies]="policies"
[group]="group?.id" />
@if (policies.length > 0) {
<policy-table [policies]="policies" />
}
</mat-expansion-panel>
<mat-expansion-panel>

View File

@ -1,4 +1,4 @@
import { Component, inject } from '@angular/core';
import { Component, inject, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Group } from '../../shared/models/group';
import { Policy } from '../../shared/models/policy';
@ -13,9 +13,10 @@ import { PolicyTableComponent } from "../../policies/policy-table/policy-table.c
import { PolicyAddButtonComponent } from '../../policies/policy-add-button/policy-add-button.component';
import { HermesClientService } from '../../hermes-client.service';
import { GroupChatter } from '../../shared/models/group-chatter';
import { TwitchUsersModule } from "../../twitch-users/twitch-users.module";
import { SpecialGroups } from '../../shared/utils/groups';
@Component({
selector: 'group-page',
imports: [
MatButtonModule,
MatExpansionModule,
@ -25,7 +26,9 @@ import { GroupChatter } from '../../shared/models/group-chatter';
PoliciesModule,
PolicyAddButtonComponent,
ReactiveFormsModule,
PolicyTableComponent
PolicyTableComponent,
PolicyTableComponent,
TwitchUsersModule,
],
templateUrl: './group-page.component.html',
styleUrl: './group-page.component.scss'
@ -38,6 +41,7 @@ export class GroupPageComponent {
private _chatters: GroupChatter[];
private _policies: Policy[];
isSpecialGroup = false;
groups: Group[] = [];
constructor() {
@ -57,6 +61,7 @@ export class GroupPageComponent {
}
this._group = group;
this.isSpecialGroup = SpecialGroups.includes(this.group!.name);
this._chatters = [...data['chatters'].filter((c: GroupChatter) => c.group_id == group_id)];
this._policies = [...data['policies'].filter((p: Policy) => p.group_id == group_id)];
});
@ -79,7 +84,7 @@ export class GroupPageComponent {
if (!this.group)
return;
this._client.first(d => d.d.request.type == 'delete_group' && d.d.request.data.id == this.group!.id)
this._client.first(d => d.d.request.type == 'delete_group' && d.d.request.data.group == this.group!.id)
.subscribe(async () => await this._router.navigate(['groups']));
this._client.deleteGroup(this.group.id);
}

View File

@ -6,16 +6,12 @@
<mat-menu #menu="matMenu">
<button mat-menu-item
(click)="openDialog('')">Custom Group</button>
@for (group of specialGroups; track $index) {
@if (!exists(group)) {
<button mat-menu-item
(click)="openDialog('everyone')">Everyone Group</button>
<button mat-menu-item
(click)="openDialog('subscribers')">Subscriber Group</button>
<button mat-menu-item
(click)="openDialog('moderators')">Moderator Group</button>
<button mat-menu-item
(click)="openDialog('vip')">VIP Group</button>
<button mat-menu-item
(click)="openDialog('broadcaster')">Broadcaster Group</button>
(click)="openDialog(group)">{{group[0].toUpperCase() + group.substring(1)}} Group</button>
}
}
</mat-menu>
<group-list class="groups"
[groups]="items" />

View File

@ -12,6 +12,7 @@ import { GroupItemEditComponent } from '../group-item-edit/group-item-edit.compo
import { MatMenuModule } from '@angular/material/menu';
import { HermesClientService } from '../../hermes-client.service';
import { GroupChatter } from '../../shared/models/group-chatter';
import { SpecialGroups } from '../../shared/utils/groups';
@Component({
selector: 'groups',
@ -32,6 +33,8 @@ export class GroupsComponent {
private readonly _route = inject(ActivatedRoute);
private readonly _dialog = inject(MatDialog);
readonly specialGroups = SpecialGroups;
items: { group: Group, chatters: GroupChatter[], policies: Policy[] }[] = [];
constructor() {
@ -116,4 +119,8 @@ export class GroupsComponent {
compare(a: Group, b: Group) {
return a.name.localeCompare(b.name);
}
exists(groupName: string) {
return this.items.some(g => g.group.name == groupName);
}
}

View File

@ -14,10 +14,11 @@ export interface Message {
providedIn: 'root'
})
export class HermesClientService {
pipe = new DatePipe('en-US');
private readonly pipe = new DatePipe('en-US');
session_id: string | undefined;
connected: boolean;
logged_in: boolean;
api_key: string | undefined;
constructor(private socket: HermesSocketService, private events: EventService) {
this.connected = false;
@ -40,6 +41,7 @@ export class HermesClientService {
this.connected = false;
this.logged_in = false;
this.session_id = undefined;
this.api_key = undefined;
this.socket.close();
this.events.emit('tts_logoff', null);
}
@ -78,6 +80,8 @@ export class HermesClientService {
if (this.logged_in)
return;
this.api_key = api_key;
this.send(1, {
api_key,
web_login: true,
@ -97,6 +101,17 @@ export class HermesClientService {
});
}
public createGroupChatter(groupId: string, chatterId: string, chatterLabel: string) {
if (!this.logged_in)
return;
this.send(3, {
request_id: null,
type: "create_group_chatter",
data: { group: groupId, chatter: chatterId, label: chatterLabel },
});
}
public createPolicy(groupId: string, path: string, usage: number, timespan: number) {
if (!this.logged_in)
return;
@ -157,6 +172,17 @@ export class HermesClientService {
});
}
public deleteGroupChatter(groupId: string, chatterId: string) {
if (!this.logged_in)
return;
this.send(3, {
request_id: null,
type: "delete_group_chatter",
data: { group: groupId, chatter: chatterId },
});
}
public deletePolicy(id: string) {
if (!this.logged_in)
return;
@ -363,6 +389,9 @@ export class HermesClientService {
console.log("TTS Heartbeat received. Potential connection problem?");
break;
case 2: // Login Ack
if (message.d.another_client) {
return;
}
console.log("TTS Login successful.");
this.logged_in = true;
this.session_id = message.d.session_id;

View File

@ -51,7 +51,7 @@
}
</mat-form-field>
</mat-card-content>
<mat-card-actions>
<mat-card-actions align="end">
@if (isNew) {
<button mat-button
(click)="save()">

View File

@ -89,7 +89,13 @@ export class PolicyItemEditComponent implements OnInit, AfterViewInit {
if (this.isNew) {
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'create_policy' && d.d.data.group_id == group_id && d.d.data.path == path && d.d.data.usage == usage && d.d.data.span == span)
.subscribe({
next: (d) => this.dialogRef.close(d.d.data),
next: (d) => {
if (d.d.error) {
// TODO: update & show response error message.
} else {
this.dialogRef.close(d.d.data);
}
},
error: () => this.waitForResponse = false,
complete: () => this.waitForResponse = false,
});
@ -97,7 +103,13 @@ export class PolicyItemEditComponent implements OnInit, AfterViewInit {
} else {
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'update_policy' && d.d.data.id == this.data.policy_id && d.d.data.group_id == group_id && d.d.data.path == path && d.d.data.usage == usage && d.d.data.span == span)
.subscribe({
next: (d) => this.dialogRef.close(d.d.data),
next: (d) => {
if (d.d.error) {
// TODO: update & show response error message.
} else {
this.dialogRef.close(d.d.data);
}
},
error: () => this.waitForResponse = false,
complete: () => this.waitForResponse = false,
});

View File

@ -91,10 +91,9 @@ export class RedemptionItemEditComponent implements OnInit {
next: (d) => {
if (d.d.error) {
this.responseError = d.d.error;
return;
}
} else {
this.dialogRef.close(id);
}
},
error: () => { this.responseError = 'Failed to receive response back from server.'; this.waitForResponse = false; },
complete: () => this.waitForResponse = false,
@ -123,11 +122,10 @@ export class RedemptionItemEditComponent implements OnInit {
next: (d) => {
if (d.d.error) {
this.responseError = d.d.error;
return;
}
} else {
this.redemption.order = order;
this.dialogRef.close(d.d.data);
}
},
error: () => { this.responseError = 'Failed to receive response back from server.'; this.waitForResponse = false; },
complete: () => this.waitForResponse = false,
@ -139,10 +137,10 @@ export class RedemptionItemEditComponent implements OnInit {
next: (d) => {
if (d.d.error) {
this.responseError = d.d.error;
return;
}
} else {
this.redemption.order = order;
this.dialogRef.close(d.d.data);
}
},
error: () => { this.responseError = 'Failed to receive response back from server.'; this.waitForResponse = false; },
complete: () => this.waitForResponse = false,

View File

@ -0,0 +1 @@
export const SpecialGroups = ['everyone', 'subscribers', 'moderators', 'vip', 'broadcaster'];

View File

@ -0,0 +1,17 @@
export function containsLettersInOrder(value: string, inside: string): boolean {
return containsLettersInOrderInternal(value, inside, 0, 0);
}
function containsLettersInOrderInternal(value: string, inside: string, indexValue: number, indexInside: number): boolean {
if (indexInside >= inside.length) {
return true;
}
if (indexValue >= value.length) {
return false;
}
const match = value.at(indexValue) == inside.at(indexInside);
return containsLettersInOrderInternal(value, inside, indexValue + 1, indexInside + (match ? 1 : 0));
}

View File

@ -0,0 +1,23 @@
<mat-card>
<mat-card-header>
<mat-card-title-group>
<mat-card-title>Add Twitch User to Group</mat-card-title>
<mat-card-subtitle>Adding to ...</mat-card-subtitle>
</mat-card-title-group>
</mat-card-header>
<mat-card-content>
<mat-form-field>
<input matInput
[formControl]="usernameControl" />
</mat-form-field>
</mat-card-content>
<mat-card-actions class="actions">
<button mat-raised-button
(click)="dialogRef.close()">Cancel</button>
<button mat-raised-button
disabled="{{waitForResponse}}"
(click)="submit()">Add</button>
</mat-card-actions>
</mat-card>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TwitchUserItemAddComponent } from './twitch-user-item-add.component';
describe('TwitchUserItemAddComponent', () => {
let component: TwitchUserItemAddComponent;
let fixture: ComponentFixture<TwitchUserItemAddComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TwitchUserItemAddComponent]
})
.compileComponents();
fixture = TestBed.createComponent(TwitchUserItemAddComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,81 @@
import { HttpClient } from '@angular/common/http';
import { Component, inject, OnInit } from '@angular/core';
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ActionItemEditComponent } from '../../actions/action-item-edit/action-item-edit.component';
import { HermesClientService } from '../../hermes-client.service';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { Group } from '../../shared/models/group';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { group } from 'console';
@Component({
selector: 'app-twitch-user-item-add',
imports: [
MatButtonModule,
MatCardModule,
MatIconModule,
MatFormFieldModule,
MatInputModule,
ReactiveFormsModule,
],
templateUrl: './twitch-user-item-add.component.html',
styleUrl: './twitch-user-item-add.component.scss'
})
export class TwitchUserItemAddComponent implements OnInit {
private readonly client = inject(HermesClientService);
private readonly data = inject<{ username: string, group: Group }>(MAT_DIALOG_DATA);
private readonly http = inject(HttpClient);
readonly usernameControl = new FormControl('', [Validators.required]);
readonly dialogRef = inject(MatDialogRef<ActionItemEditComponent>);
waitForResponse = false;
ngOnInit(): void {
this.usernameControl.setValue(this.data.username);
}
submit() {
if (this.usernameControl.invalid || this.waitForResponse || !this.client.api_key) {
return;
}
this.waitForResponse = true;
const username = this.usernameControl.value!.toLowerCase();
this.http.get('/api/auth/twitch/users?login=' + username, {
headers: {
'x-api-key': this.client.api_key,
}
})
.subscribe((response: any) => {
if (!response.user) {
this.waitForResponse = false;
return;
}
if (!response.user) {
return;
}
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'create_group_chatter' && d.d.request.data.chatter == response.user.id)
.subscribe({
next: (d) => {
if (d.d.error) {
// TODO: update & show response error message.
} else {
this.dialogRef.close(d.d.data);
}
},
error: () => this.waitForResponse = false,
complete: () => this.waitForResponse = false,
});
this.client.createGroupChatter(this.data.group.id, response.user.id, response.user.login)
});
}
}

View File

@ -0,0 +1,7 @@
<div>
<button mat-icon-button
(click)="delete()">
<mat-icon>remove</mat-icon>
</button>
<p>{{user.chatter_label}}</p>
</div>

View File

@ -0,0 +1,18 @@
div {
padding: 0.3em;
}
p {
position: relative;
top: -7px;
margin-left: 5px;
display: inline;
}
.mat-icon {
color: #C83838;
}
.mat-icon:hover {
color: red;
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TwitchUserItemComponent } from './twitch-user-item.component';
describe('TwitchUserItemComponent', () => {
let component: TwitchUserItemComponent;
let fixture: ComponentFixture<TwitchUserItemComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TwitchUserItemComponent]
})
.compileComponents();
fixture = TestBed.createComponent(TwitchUserItemComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,39 @@
import { Component, inject, Input } from '@angular/core';
import { GroupChatter } from '../../shared/models/group-chatter';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { HermesClientService } from '../../hermes-client.service';
import EventService from '../../shared/services/EventService';
@Component({
selector: 'twitch-user-item',
imports: [
MatButtonModule,
MatIconModule,
MatInputModule
],
templateUrl: './twitch-user-item.component.html',
styleUrl: './twitch-user-item.component.scss'
})
export class TwitchUserItemComponent {
@Input({ required: true }) user: GroupChatter = { chatter_id: -1, chatter_label: '', user_id: '', group_id: '' };
private readonly _client = inject(HermesClientService);
private readonly _events = inject(EventService);
private _deleted = false;
delete() {
if (this._deleted)
return;
this._deleted = true;
this._client.first(d => d.d.request.type == 'delete_group_chatter' && d.d.request.data.group == this.user.group_id && d.d.request.data.chatter == this.user.chatter_id)
.subscribe(async (response) => {
console.log('delete group chatter', response)
this._events.emit('delete_group_chatter', this.user);
});
this._client.deleteGroupChatter(this.user.group_id, this.user.chatter_id.toString());
}
}

View File

@ -0,0 +1,28 @@
<ul>
<li class="header">
<mat-form-field appearance="outline"
subscriptSizing="dynamic">
<mat-label>Filter</mat-label>
<input matInput
placeholder="Filter Twitch usernames"
[formControl]="searchControl" />
</mat-form-field>
<button mat-icon-button
(click)="add()">
<mat-icon>person_add</mat-icon>
</button>
</li>
@for (user of users; track $index) {
<li>
<twitch-user-item [user]="user" />
</li>
}
@if (!users.length) {
@if (searchControl.value) {
<p class="notice">No users fits the filter.</p>
} @else {
<p class="notice">No users in this group.</p>
}
}
</ul>

View File

@ -0,0 +1,38 @@
@use '@angular/material' as mat;
ul {
@include mat.all-component-densities(-5);
@include mat.form-field-overrides((
outlined-outline-color: rgb(167, 88, 199),
outlined-focus-label-text-color: rgb(155, 57, 194),
outlined-focus-outline-color: rgb(155, 57, 194),
));
background-color: rgb(202, 68, 255);
border-radius: 15px;
margin: 0 0;
padding: 0;
max-width: 500px;
overflow: hidden;
}
ul li {
margin: 0;
padding: 0;
list-style: none;
background-color: rgb(240, 165, 255);
}
ul li.header {
background-color: rgb(215, 115, 255);
display: flex;
align-items: center;
justify-content: space-around;
flex-direction: row;
padding: 8px;
}
ul .notice {
text-align: center;
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TwitchUserListComponent } from './twitch-user-list.component';
describe('TwitchUserListComponent', () => {
let component: TwitchUserListComponent;
let fixture: ComponentFixture<TwitchUserListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TwitchUserListComponent]
})
.compileComponents();
fixture = TestBed.createComponent(TwitchUserListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,68 @@
import { Component, inject, Input } from '@angular/core';
import { GroupChatter } from '../../shared/models/group-chatter';
import { TwitchUserItemComponent } from "../twitch-user-item/twitch-user-item.component";
import { MatInputModule } from '@angular/material/input';
import { MatIconModule } from '@angular/material/icon';
import { MatFormFieldModule } from '@angular/material/form-field';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { containsLettersInOrder } from '../../shared/utils/string-compare';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { HermesClientService } from '../../hermes-client.service';
import { Group } from '../../shared/models/group';
import { TwitchUserItemAddComponent } from '../twitch-user-item-add/twitch-user-item-add.component';
import EventService from '../../shared/services/EventService';
@Component({
selector: 'twitch-user-list',
imports: [
MatButtonModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
ReactiveFormsModule,
TwitchUserItemComponent,
],
templateUrl: './twitch-user-list.component.html',
styleUrl: './twitch-user-list.component.scss'
})
export class TwitchUserListComponent {
@Input({ required: true }) twitchUsers: GroupChatter[] = [];
@Input() group: Group | undefined;
readonly dialog = inject(MatDialog);
readonly client = inject(HermesClientService);
readonly events = inject(EventService);
readonly searchControl: FormControl = new FormControl('');
opened = false;
constructor() {
this.events.listen('delete_group_chatter', (chatter: GroupChatter) => {
this.twitchUsers.splice(this.twitchUsers.findIndex(c => c.group_id == chatter.group_id && c.chatter_id == chatter.chatter_id), 1);
});
}
get users(): GroupChatter[] {
return this.twitchUsers.filter(u => containsLettersInOrder(u.chatter_label, this.searchControl.value));
}
add() {
if (this.opened)
return;
this.opened = true;
const dialogRef = this.dialog.open(TwitchUserItemAddComponent, {
data: { username: this.searchControl.value, group: this.group },
});
dialogRef.afterClosed().subscribe((chatter: GroupChatter) => {
this.opened = false;
if (!chatter)
return;
this.twitchUsers.push(chatter);
});
}
}

View File

@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TwitchUserItemComponent } from './twitch-user-item/twitch-user-item.component';
import { TwitchUserListComponent } from './twitch-user-list/twitch-user-list.component';
@NgModule({
declarations: [],
exports: [
TwitchUserItemComponent,
TwitchUserListComponent,
],
imports: [
TwitchUserItemComponent,
TwitchUserListComponent,
]
})
export class TwitchUsersModule { }

View File

@ -0,0 +1 @@
<p>user-item works!</p>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserItemComponent } from './user-item.component';
describe('UserItemComponent', () => {
let component: UserItemComponent;
let fixture: ComponentFixture<UserItemComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserItemComponent]
})
.compileComponents();
fixture = TestBed.createComponent(UserItemComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-user-item',
imports: [],
templateUrl: './user-item.component.html',
styleUrl: './user-item.component.scss'
})
export class UserItemComponent {
}

View File

@ -0,0 +1 @@
<p>user-list works!</p>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserListComponent } from './user-list.component';
describe('UserListComponent', () => {
let component: UserListComponent;
let fixture: ComponentFixture<UserListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserListComponent]
})
.compileComponents();
fixture = TestBed.createComponent(UserListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-user-list',
imports: [],
templateUrl: './user-list.component.html',
styleUrl: './user-list.component.scss'
})
export class UserListComponent {
}

View File

@ -0,0 +1,12 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@NgModule({
declarations: [],
imports: [
CommonModule
]
})
export class UsersModule { }