GroupDeletedEvent + GroupLeftEvent + removed UI Subscriber

This commit is contained in:
Patrick Haßel 2024-11-06 11:42:34 +01:00
parent 452e5f27f6
commit e7dba8139b
16 changed files with 216 additions and 130 deletions

View File

@ -1,50 +0,0 @@
import {Subscription} from "rxjs";
import {Next} from "./common/types";
export class Subscribed<T> {
private subscription: Subscription | null = null;
private _value: T | null = null;
constructor(
private readonly same: (a: T, b: T) => boolean,
private readonly subscribe: (value: T, next: Next<T>) => Subscription,
) {
// -
}
get value(): T | null {
return this._value;
}
set value(value: T | null) {
if (!this.isSame(value)) {
this.unsubscribe();
if (value) {
this.subscription = this.subscribe(value, next => this.value = next);
}
}
this._value = value;
}
public unsubscribe() {
if (this.subscription) {
this.subscription.unsubscribe();
this.subscription = null;
}
}
private isSame(value: T | null) {
if (this._value === null) {
return value === null;
} else {
if (value === null) {
return false
} else {
return this.same(this._value, value);
}
}
}
}

View File

@ -1,13 +1,15 @@
import {Injectable} from '@angular/core';
import {Injectable, Type} from '@angular/core';
import {ApiService} from "../common/api.service";
import {UserPrivate} from "./UserPrivate";
import {Next} from "../common/types";
import {UserPublic} from "./UserPublic";
import {EventType, Router} from "@angular/router";
import {BehaviorSubject, filter, Subject, Subscription} from "rxjs";
import {BehaviorSubject, filter, map, Subject, Subscription} from "rxjs";
import {Group} from "../group/Group";
import {StompService} from "@stomp/ng2-stompjs";
import {Numbers} from "../tools/Numbers/Numbers";
import {GroupDeletedEvent} from "../group/events/GroupDeletedEvent";
import {GroupLeftEvent} from "../group/events/GroupLeftEvent";
function userPushMessageFromJson(json: any): object {
const type = json['_type_'];
@ -18,6 +20,10 @@ function userPushMessageFromJson(json: any): object {
return UserPrivate.fromJson(json['payload']);
case 'GroupDto':
return Group.fromJson(json['payload']);
case 'GroupDeletedEvent':
return GroupDeletedEvent.fromJson(json['payload']);
case 'GroupLeftEvent':
return GroupLeftEvent.fromJson(json['payload']);
}
throw new Error("Not implemented UserPushMessage._type_ = " + type);
}
@ -78,12 +84,6 @@ export class UserService {
return this.subject.subscribe(next);
}
subscribePush<T>(predicate: (m: any) => boolean, next: Next<T>): Subscription {
return this.pushSubject
.pipe(filter(predicate))
.subscribe(next);
}
iOwn(group: Group): boolean {
return group.owner.equals(this.user);
}
@ -103,4 +103,13 @@ export class UserService {
return this.user !== null && this.user.equals(user);
}
subscribePush<T>(TYPE: Type<T>, next: Next<T>): Subscription {
return this.pushSubject
.pipe(
filter(m => m instanceof TYPE),
map(m => m as T),
)
.subscribe(next);
}
}

View File

@ -1,11 +1,12 @@
import {UserPublic} from "../User/UserPublic";
import {validateBoolean, validateDate, validateList, validateString} from "../common/validators";
import {UserPrivate} from "../User/UserPrivate";
import {GroupUuid} from "./GroupUuid";
export class Group {
export class Group extends GroupUuid {
protected constructor(
readonly uuid: string,
constructor(
uuid: string,
readonly owner: UserPublic,
readonly created: Date,
readonly title: string,
@ -14,7 +15,7 @@ export class Group {
readonly banned: UserPublic[],
readonly initial: boolean,
) {
// -
super(uuid);
}
isOwner(user: UserPublic) {
@ -47,10 +48,6 @@ export class Group {
return UserPublic.compareName(a, b);
}
static sameUuid(a: Group, b: Group): boolean {
return a.uuid === b.uuid;
}
static compareCreated(a: Group, b: Group): number {
return a.created.getTime() - b.created.getTime();
}

View File

@ -0,0 +1,13 @@
export class GroupUuid {
protected constructor(
readonly uuid: string,
) {
// -
}
is(other: GroupUuid | null) {
return other !== null && this.uuid === other.uuid;
}
}

View File

@ -0,0 +1,19 @@
import {validateString} from "../../common/validators";
import {GroupUuid} from "../GroupUuid";
export class GroupDeletedEvent extends GroupUuid {
constructor(
uuid: string,
) {
super(uuid);
}
static fromJson(json: any): GroupDeletedEvent {
return new GroupDeletedEvent(
validateString(json['uuid']),
);
}
}

View File

@ -0,0 +1,19 @@
import {GroupUuid} from "../GroupUuid";
import {validateString} from "../../common/validators";
import {GroupDeletedEvent} from "./GroupDeletedEvent";
export class GroupLeftEvent extends GroupUuid {
constructor(
uuid: string,
) {
super(uuid);
}
static fromJson(json: any): GroupDeletedEvent {
return new GroupDeletedEvent(
validateString(json['uuid']),
);
}
}

View File

@ -5,7 +5,6 @@ import {Router} from "@angular/router";
import {UserService} from "../User/user.service";
import {validateBoolean} from "../common/validators";
import {Injectable} from "@angular/core";
import {Subscribed} from "../Subscribed";
import {UserPublic} from "../User/UserPublic";
import {GroupUserRequest} from "./requests/GroupUserRequest";
import {GroupChangeTitleRequest} from "./requests/GroupChangeTitleRequest";
@ -68,10 +67,6 @@ export class GroupService {
this.router.navigate(['Group', uuid]);
}
newSubscriber(): Subscribed<Group> {
return new Subscribed<Group>(Group.sameUuid, (group, next) => this.api.subscribe(['Group', group.uuid], Group.fromJson, next));
}
gotoGroups(): void {
this.router.navigate(['Groups']);
}

View File

@ -17,9 +17,7 @@ export class NumbersService {
protected readonly router: Router,
protected readonly userService: UserService
) {
this.userService.subscribePush<Numbers>(m => m instanceof Numbers, message => {
this.router.navigate(['Numbers', message.uuid])
});
// -
}
create(groupUuid: string): void {

View File

@ -1,6 +1,6 @@
<div class="tileContainer">
<div class="tile" *ngIf="group.value === null && granted === false">
<div class="tile" *ngIf="group === null && granted === false">
<div class="tileInner">
<div class="tileTitle">
Passwort
@ -11,7 +11,7 @@
</div>
</div>
<ng-container *ngIf="group.value">
<ng-container *ngIf="group">
<div class="tile">
<div class="tileInner">
@ -23,26 +23,26 @@
<table>
<tr>
<th>Erstellt</th>
<td>{{ group.value.created | date:'yyyy-MM-dd HH:mm' }}</td>
<td>{{ group.created | date:'yyyy-MM-dd HH:mm' }}</td>
</tr>
<tr>
<th>Titel</th>
<td>
<app-text [initial]="group.value.title" [editable]="userService.iOwn(group.value)" (onChange)="changeTitle(group.value, $event)"></app-text>
<ng-container *ngIf="!userService.iOwn(group.value)"></ng-container>
<app-text [initial]="group.title" [editable]="userService.iOwn(group)" (onChange)="changeTitle(group, $event)"></app-text>
<ng-container *ngIf="!userService.iOwn(group)"></ng-container>
</td>
</tr>
<tr>
<th>Passwort</th>
<td>
<app-text [initial]="group.value.password" [editable]="userService.iOwn(group.value)" (onChange)="changePassword(group.value, $event)"></app-text>
<ng-container *ngIf="!userService.iOwn(group.value)"></ng-container>
<app-text [initial]="group.password" [editable]="userService.iOwn(group)" (onChange)="changePassword(group, $event)"></app-text>
<ng-container *ngIf="!userService.iOwn(group)"></ng-container>
</td>
</tr>
</table>
<div class="buttons">
<div class="button buttonRight buttonDelete" (click)="groupDelete(group.value)">
<div class="button buttonRight buttonDelete" (click)="groupDelete(group)">
Gruppe löschen
</div>
</div>
@ -59,16 +59,16 @@
<div class="tileContent">
<div class="numbers">
<table>
<tr [class.user_owner]="group.value.isOwner(user)" *ngFor="let user of group.value.usersByNameOwnerFirst()">
<tr [class.user_owner]="group.isOwner(user)" *ngFor="let user of group.usersByNameOwnerFirst()">
<td (click)="userService.goto(user)">{{ user.name }}</td>
<td>
<span class="owner" *ngIf="group.value.isOwnedBy(user)">
<span class="owner" *ngIf="group.isOwnedBy(user)">
Admin
</span>
<div class="buttons">
<ng-container *ngIf="userService.iOwn(group.value) && !userService.iAm(user)">
<div class="button buttonRight buttonBan" (click)="groupService.ban(group.value, user)">Verbannen</div>
<div class="button buttonRight buttonRemove" (click)="groupService.kick(group.value, user)">Entfernen</div>
<ng-container *ngIf="userService.iOwn(group) && !userService.iAm(user)">
<div class="button buttonRight buttonBan" (click)="groupService.ban(group, user)">Verbannen</div>
<div class="button buttonRight buttonRemove" (click)="groupService.kick(group, user)">Entfernen</div>
</ng-container>
</div>
</td>
@ -79,7 +79,7 @@
</div>
</div>
<div class="tile" *ngIf="group.value.banned.length > 0">
<div class="tile" *ngIf="group.banned.length > 0">
<div class="tileInner">
<div class="tileTitle">
Verbannt
@ -87,12 +87,12 @@
<div class="tileContent">
<div class="numbers">
<table>
<tr [class.user_owner]="group.value.isOwner(user)" *ngFor="let user of group.value.bannedByName()" (click)="userService.goto(user)">
<tr [class.user_owner]="group.isOwner(user)" *ngFor="let user of group.bannedByName()" (click)="userService.goto(user)">
<td>{{ user.name }}</td>
<td>
<div class="buttons">
<ng-container *ngIf="userService.iOwn(group.value) && !userService.iAm(user)">
<div class="button buttonRight buttonUnban" (click)="groupService.unban(group.value, user)">Aufheben</div>
<ng-container *ngIf="userService.iOwn(group) && !userService.iAm(user)">
<div class="button buttonRight buttonUnban" (click)="groupService.unban(group, user)">Aufheben</div>
</ng-container>
</div>
</td>
@ -110,7 +110,7 @@
</div>
<div class="tileContent">
<div class="buttons">
<div class="button buttonRight buttonNext" *ngIf="userService.iOwn(group.value)" (click)="numbersService.create(group.value.uuid)">+ Nächste Runde</div>
<div class="button buttonRight buttonNext" *ngIf="userService.iOwn(group)" (click)="numbersService.create(group.uuid)">+ Nächste Runde</div>
</div>
<div class="numbers">
<table>

View File

@ -6,13 +6,14 @@ import {ActivatedRoute, Router} from "@angular/router";
import {GroupService} from "../../../api/group/group.service";
import {UserService} from "../../../api/User/user.service";
import {Group} from "../../../api/group/Group";
import {Subscribed} from "../../../api/Subscribed";
import {NumbersService} from "../../../api/tools/Numbers/numbers.service";
import {PasswordComponent} from "../../../shared/password/password.component";
import {Numbers} from "../../../api/tools/Numbers/Numbers";
import {Page} from "../../../api/common/Page";
import {RelativePipe} from "../../../shared/relative.pipe";
import {Subscription, timer} from "rxjs";
import {GroupDeletedEvent} from "../../../api/group/events/GroupDeletedEvent";
import {GroupLeftEvent} from "../../../api/group/events/GroupLeftEvent";
@Component({
selector: 'app-group',
@ -32,7 +33,9 @@ import {Subscription, timer} from "rxjs";
})
export class GroupComponent implements OnInit, OnDestroy {
protected readonly group: Subscribed<Group>;
protected readonly subs: Subscription[] = [];
protected group: Group | null = null;
protected numbersList: Page<Numbers> = Page.EMPTY;
@ -42,8 +45,6 @@ export class GroupComponent implements OnInit, OnDestroy {
protected now: Date = new Date();
private timer?: Subscription;
constructor(
protected readonly router: Router,
protected readonly activatedRoute: ActivatedRoute,
@ -51,12 +52,30 @@ export class GroupComponent implements OnInit, OnDestroy {
protected readonly userService: UserService,
protected readonly numbersService: NumbersService,
) {
this.group = this.groupService.newSubscriber();
// -
}
ngOnInit(): void {
this.timer = timer(1000, 1000).subscribe(() => this.now = new Date());
this.activatedRoute.params.subscribe(params => {
this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date()));
this.subs.push(this.userService.subscribePush(Numbers, _ => {
this.updateNumbersList();
}));
this.subs.push(this.userService.subscribePush(Group, group => {
if (group.is(this.group)) {
this.group = group;
}
}));
this.subs.push(this.userService.subscribePush(GroupDeletedEvent, event => {
if (event.is(this.group)) {
this.groupService.gotoGroups();
}
}));
this.subs.push(this.userService.subscribePush(GroupLeftEvent, event => {
if (event.is(this.group)) {
this.groupService.gotoGroups();
}
}));
this.subs.push(this.activatedRoute.params.subscribe(params => {
const groupUuid = params['uuid'];
this.uuid = groupUuid;
this.granted = null;
@ -64,33 +83,37 @@ export class GroupComponent implements OnInit, OnDestroy {
this.groupService.canAccess(groupUuid, granted => {
this.granted = granted;
if (granted) {
this.groupService.get(groupUuid, group => this.group.value = group);
this.numbersService.page(groupUuid, 0, 10, numbersList => this.numbersList = numbersList);
this.groupService.get(groupUuid, group => {
this.group = group;
this.updateNumbersList();
});
}
});
}
});
}));
}
private updateNumbersList() {
if (this.group) {
this.numbersService.page(this.group.uuid, 0, 10, numbersList => this.numbersList = numbersList);
}
}
ngOnDestroy(): void {
if (this.timer) {
this.timer.unsubscribe();
this.timer = undefined;
}
this.group.unsubscribe();
this.subs.forEach(sub => sub.unsubscribe());
}
protected changeTitle(group: Group, title: string): void {
this.groupService.changeTitle(group, title, group => this.group.value = group);
this.groupService.changeTitle(group, title, group => this.group = group);
}
protected changePassword(group: Group, password: string): void {
this.groupService.changePassword(group, password, group => this.group.value = group);
this.groupService.changePassword(group, password, group => this.group = group);
}
protected join(password: string): void {
if (this.uuid) {
this.groupService.join(this.uuid, password, group => this.group.value = group);
this.groupService.join(this.uuid, password, group => this.group = group);
}
}

View File

@ -6,6 +6,8 @@ import {GroupService} from "../../../api/group/group.service";
import {Group} from "../../../api/group/Group";
import {Subscription} from "rxjs";
import {ReactiveFormsModule} from "@angular/forms";
import {GroupLeftEvent} from "../../../api/group/events/GroupLeftEvent";
import {GroupDeletedEvent} from "../../../api/group/events/GroupDeletedEvent";
@Component({
selector: 'app-groups',
@ -20,7 +22,7 @@ import {ReactiveFormsModule} from "@angular/forms";
})
export class GroupsComponent implements OnInit, OnDestroy {
private readonly subscriptions: Subscription[] = [];
private readonly subs: Subscription[] = [];
protected groups: Group[] = [];
@ -32,13 +34,18 @@ export class GroupsComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
this.subscriptions.push(this.userService.subscribe(_ => {
this.groupService.findAllJoined(groups => this.groups = groups);
}));
this.subs.push(this.userService.subscribePush(Group, _ => this.updateGroups()));
this.subs.push(this.userService.subscribePush(GroupLeftEvent, _ => this.updateGroups()));
this.subs.push(this.userService.subscribePush(GroupDeletedEvent, _ => this.updateGroups()));
this.updateGroups();
}
ngOnDestroy(): void {
this.subscriptions.forEach(subscription => subscription.unsubscribe());
this.subs.forEach(sub => sub.unsubscribe());
}
private updateGroups() {
this.groupService.findAllJoined(groups => this.groups = groups);
}
create(): void {

View File

@ -1,10 +1,12 @@
package de.ph87.tools.group;
import de.ph87.tools.group.dto.GroupDto;
import de.ph87.tools.group.events.GroupLeftEvent;
import de.ph87.tools.group.requests.GroupJoinRequest;
import de.ph87.tools.group.uuid.GroupUuid;
import de.ph87.tools.user.User;
import de.ph87.tools.user.UserService;
import de.ph87.tools.user.push.UserPushService;
import de.ph87.tools.user.uuid.UserPrivateUuid;
import jakarta.annotation.Nullable;
import jakarta.servlet.http.HttpServletResponse;
@ -16,6 +18,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.util.function.BiConsumer;
@Slf4j
@Service
@Transactional
@ -26,6 +30,8 @@ public class GroupMemberService {
private final GroupReadService groupReadService;
private final UserPushService userPushService;
@NonNull
public GroupDto join(@Nullable final UserPrivateUuid privateUuid, @NonNull final GroupJoinRequest request, @NonNull final HttpServletResponse response) {
final User user = userService.getUserByPrivateUuidOrElseCreate(privateUuid, response);
@ -38,7 +44,7 @@ public class GroupMemberService {
log.error("Wrong password: user={}, group={}", user, group);
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
return _join_unchecked(group, user);
return _add_user_to_group_unchecked(group, user);
}
public void leave(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUuid groupUuid) {
@ -48,11 +54,11 @@ public class GroupMemberService {
// owner cannot remove itself from group
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
_leave_unchecked(group, user);
_remove_user_from_group_unchecked(group, user, (g, u) -> log.info("User left Group: user={}, group={}", u, g));
}
@NonNull
public GroupDto _join_unchecked(@NonNull final Group group, @NonNull final User user) {
public GroupDto _add_user_to_group_unchecked(@NonNull final Group group, @NonNull final User user) {
group.getUsers().add(user);
group.touch();
user.touch();
@ -60,11 +66,12 @@ public class GroupMemberService {
return groupReadService.publish(group);
}
private void _leave_unchecked(final Group group, final User user) {
public void _remove_user_from_group_unchecked(@NonNull final Group group, @NonNull final User user, @NonNull final BiConsumer<Group, User> beforePush) {
group.getUsers().remove(user);
group.touch();
user.touch();
log.info("User left Group: user={}, group={}", user, group);
beforePush.accept(group, user);
userPushService.push(user, new GroupLeftEvent(group));
groupReadService.publish(group);
}

View File

@ -23,6 +23,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.util.function.BiConsumer;
@Slf4j
@Service
@Transactional
@ -47,7 +49,7 @@ public class GroupOwnerService {
public GroupDto create(@Nullable final UserPrivateUuid privateUuid, @NonNull final HttpServletResponse response) {
final User user = userService.getUserByPrivateUuidOrElseCreate(privateUuid, response);
final Group group = _create_unchecked(user);
return groupMemberService._join_unchecked(group, user);
return groupMemberService._add_user_to_group_unchecked(group, user);
}
public void delete(@NonNull final UserPrivateUuid userPrivateUuid, @NonNull final GroupUuid groupUuid) {
@ -61,15 +63,13 @@ public class GroupOwnerService {
}
public void kick(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUserRequest request) {
final OwnerRemoveResult result = _removeUser(privateUuid, request);
log.info("User kicked out of group: user={}, group={}", result.kicked, result.group);
final OwnerRemoveResult result = _removeUser(privateUuid, request, (group, user) -> log.info("User kicked out of group: user={}, group={}", user, group));
groupReadService.publish(result.group);
}
public void ban(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUserRequest request) {
final OwnerRemoveResult result = _removeUser(privateUuid, request);
final OwnerRemoveResult result = _removeUser(privateUuid, request, (group, user) -> log.info("User banned from group: user={}, group={}", user, group));
result.group.getBanned().add(result.kicked);
log.info("User banned from group: user={}, group={}", result.kicked, result.group);
groupReadService.publish(result.group);
}
@ -108,14 +108,14 @@ public class GroupOwnerService {
}
@NonNull
private GroupOwnerService.OwnerRemoveResult _removeUser(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUserRequest request) {
private OwnerRemoveResult _removeUser(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUserRequest request, @NonNull final BiConsumer<Group, User> beforePush) {
final GroupAccess access = groupAccessService.ownerAccess(privateUuid, request.groupUuid);
final User user = access.group.getUsers().stream().filter(u -> u.getPublicUuid().equals(request.userPublicUuid)).findFirst().orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
if (user.equals(access.user)) {
// owner cannot kick itself from group
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
access.group.getUsers().remove(user);
groupMemberService._remove_user_from_group_unchecked(access.group, user, beforePush);
return new OwnerRemoveResult(access, user);
}

View File

@ -1,5 +1,6 @@
package de.ph87.tools.group.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import de.ph87.tools.common.uuid.UuidSerializer;
import de.ph87.tools.group.Group;
@ -27,17 +28,40 @@ public class GroupDto {
public final ZonedDateTime created;
@NonNull
@ToString.Exclude
public final String password;
@NonNull
@ToString.Exclude
public final UserPublicDto owner;
@NonNull
public final Set<UserPublicDto> users;
@JsonIgnore
@ToString.Include
public String ownerPublicUuid() {
return owner.publicUuid.uuid;
}
@NonNull
@ToString.Exclude
public final Set<UserPublicDto> users;
@JsonIgnore
@ToString.Include
public int users() {
return users.size();
}
@NonNull
@ToString.Exclude
public final Set<UserPublicDto> banned;
@JsonIgnore
@ToString.Include
public int banned() {
return banned.size();
}
public final boolean initial;
public GroupDto(@NonNull final Group group, @NonNull final UserPublicDto owner, @NonNull final Set<UserPublicDto> users, @NonNull final Set<UserPublicDto> banned) {

View File

@ -1,5 +1,7 @@
package de.ph87.tools.group.events;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import de.ph87.tools.common.uuid.UuidSerializer;
import de.ph87.tools.group.Group;
import de.ph87.tools.group.uuid.GroupUuid;
import lombok.Getter;
@ -10,10 +12,11 @@ import lombok.ToString;
@ToString
public class GroupDeletedEvent {
public final GroupUuid groupUuid;
@JsonSerialize(using = UuidSerializer.class)
public final GroupUuid uuid;
public GroupDeletedEvent(@NonNull final Group group) {
this.groupUuid = group.getUuid();
this.uuid = group.getUuid();
}
}

View File

@ -0,0 +1,22 @@
package de.ph87.tools.group.events;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import de.ph87.tools.common.uuid.UuidSerializer;
import de.ph87.tools.group.Group;
import de.ph87.tools.group.uuid.GroupUuid;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
@Getter
@ToString
public class GroupLeftEvent {
@JsonSerialize(using = UuidSerializer.class)
public final GroupUuid uuid;
public GroupLeftEvent(@NonNull final Group group) {
this.uuid = group.getUuid();
}
}