Added user management for groups. Improved user experience slightly. Added some error checks for request acks.
This commit is contained in:
parent
2f2215b041
commit
1acda7978e
@ -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"
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()">
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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}}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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" />
|
@ -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',
|
||||
@ -31,6 +32,8 @@ export class GroupsComponent {
|
||||
private readonly _client = inject(HermesClientService);
|
||||
private readonly _route = inject(ActivatedRoute);
|
||||
private readonly _dialog = inject(MatDialog);
|
||||
|
||||
readonly specialGroups = SpecialGroups;
|
||||
|
||||
items: { group: Group, chatters: GroupChatter[], policies: Policy[] }[] = [];
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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()">
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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,
|
||||
|
1
src/app/shared/utils/groups.ts
Normal file
1
src/app/shared/utils/groups.ts
Normal file
@ -0,0 +1 @@
|
||||
export const SpecialGroups = ['everyone', 'subscribers', 'moderators', 'vip', 'broadcaster'];
|
17
src/app/shared/utils/string-compare.ts
Normal file
17
src/app/shared/utils/string-compare.ts
Normal 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));
|
||||
}
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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)
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
<div>
|
||||
<button mat-icon-button
|
||||
(click)="delete()">
|
||||
<mat-icon>remove</mat-icon>
|
||||
</button>
|
||||
<p>{{user.chatter_label}}</p>
|
||||
</div>
|
@ -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;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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());
|
||||
}
|
||||
}
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
19
src/app/twitch-users/twitch-users.module.ts
Normal file
19
src/app/twitch-users/twitch-users.module.ts
Normal 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 { }
|
1
src/app/users/user-item/user-item.component.html
Normal file
1
src/app/users/user-item/user-item.component.html
Normal file
@ -0,0 +1 @@
|
||||
<p>user-item works!</p>
|
0
src/app/users/user-item/user-item.component.scss
Normal file
0
src/app/users/user-item/user-item.component.scss
Normal file
23
src/app/users/user-item/user-item.component.spec.ts
Normal file
23
src/app/users/user-item/user-item.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
11
src/app/users/user-item/user-item.component.ts
Normal file
11
src/app/users/user-item/user-item.component.ts
Normal 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 {
|
||||
|
||||
}
|
1
src/app/users/user-list/user-list.component.html
Normal file
1
src/app/users/user-list/user-list.component.html
Normal file
@ -0,0 +1 @@
|
||||
<p>user-list works!</p>
|
0
src/app/users/user-list/user-list.component.scss
Normal file
0
src/app/users/user-list/user-list.component.scss
Normal file
23
src/app/users/user-list/user-list.component.spec.ts
Normal file
23
src/app/users/user-list/user-list.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
11
src/app/users/user-list/user-list.component.ts
Normal file
11
src/app/users/user-list/user-list.component.ts
Normal 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 {
|
||||
|
||||
}
|
12
src/app/users/users.module.ts
Normal file
12
src/app/users/users.module.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
CommonModule
|
||||
]
|
||||
})
|
||||
export class UsersModule { }
|
Loading…
x
Reference in New Issue
Block a user