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

@@ -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 { }