Add Chat UI Components
Add Chat User Interface Restructure some services Virtual Scrolling Manual change detection for message updates Enhanced Date pipe Message layout Tabbed reusable chat window Deleting messages Further permission checks Delete-prompts Mobile friendly chat usage automatically scroll to bottom
This commit is contained in:
parent
8e5b1fa99d
commit
69adc1d41c
@ -1 +1 @@
|
|||||||
Subproject commit 3380911a7e9cb4a906d3729d30a164ed3d59fd22
|
Subproject commit 756043511cc00b9fd4b42cc3a7ba0d8c16897895
|
@ -6,7 +6,7 @@ import { Router } from '@angular/router';
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { filter, take } from 'rxjs/operators';
|
import { filter, take } from 'rxjs/operators';
|
||||||
|
|
||||||
import { ChatNotificationService } from './core/ui-services/chat-notification.service';
|
import { ChatNotificationService } from './site/chat/services/chat-notification.service';
|
||||||
import { ConfigService } from './core/ui-services/config.service';
|
import { ConfigService } from './core/ui-services/config.service';
|
||||||
import { ConstantsService } from './core/core-services/constants.service';
|
import { ConstantsService } from './core/core-services/constants.service';
|
||||||
import { CountUsersService } from './core/ui-services/count-users.service';
|
import { CountUsersService } from './core/ui-services/count-users.service';
|
||||||
|
@ -6,6 +6,11 @@ export class ChatMessage extends BaseModel<ChatMessage> {
|
|||||||
public id: number;
|
public id: number;
|
||||||
public text: string;
|
public text: string;
|
||||||
public username: string;
|
public username: string;
|
||||||
|
/**
|
||||||
|
* Note: Do not expect, that this user is known in the client.
|
||||||
|
* Use this id just as a numerical value.
|
||||||
|
*/
|
||||||
|
public user_id: number;
|
||||||
public timestamp: string;
|
public timestamp: string;
|
||||||
public chatgroup_id: number;
|
public chatgroup_id: number;
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import * as moment from 'moment';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* pipe to convert and translate dates
|
* pipe to convert and translate dates
|
||||||
|
* requires a "date"object
|
||||||
*/
|
*/
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'localizedDate',
|
name: 'localizedDate',
|
||||||
@ -13,13 +14,13 @@ import * as moment from 'moment';
|
|||||||
export class LocalizedDatePipe implements PipeTransform {
|
export class LocalizedDatePipe implements PipeTransform {
|
||||||
public constructor(private translate: TranslateService) {}
|
public constructor(private translate: TranslateService) {}
|
||||||
|
|
||||||
public transform(value: any, dateFormat: string = 'lll'): any {
|
public transform(date: Date, dateFormat: string = 'lll'): any {
|
||||||
const lang = this.translate.currentLang ? this.translate.currentLang : this.translate.defaultLang;
|
const lang = this.translate.currentLang ? this.translate.currentLang : this.translate.defaultLang;
|
||||||
if (!value) {
|
if (!date) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
moment.locale(lang);
|
moment.locale(lang);
|
||||||
const dateLocale = moment.unix(value).local();
|
const dateLocale = moment.unix(date.getTime() / 1000).local();
|
||||||
return dateLocale.format(dateFormat);
|
return dateLocale.format(dateFormat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { Route, RouterModule } from '@angular/router';
|
import { Route, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
import { ChatGroupDetailComponent } from './components/chat-group-detail/chat-group-detail.component';
|
|
||||||
import { ChatGroupListComponent } from './components/chat-group-list/chat-group-list.component';
|
import { ChatGroupListComponent } from './components/chat-group-list/chat-group-list.component';
|
||||||
|
|
||||||
const routes: Route[] = [
|
const routes: Route[] = [
|
||||||
@ -9,8 +8,7 @@ const routes: Route[] = [
|
|||||||
path: '',
|
path: '',
|
||||||
pathMatch: 'full',
|
pathMatch: 'full',
|
||||||
component: ChatGroupListComponent
|
component: ChatGroupListComponent
|
||||||
},
|
}
|
||||||
{ path: ':id', component: ChatGroupDetailComponent }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -3,11 +3,20 @@ import { NgModule } from '@angular/core';
|
|||||||
|
|
||||||
import { ChatGroupDetailComponent } from './components/chat-group-detail/chat-group-detail.component';
|
import { ChatGroupDetailComponent } from './components/chat-group-detail/chat-group-detail.component';
|
||||||
import { ChatGroupListComponent } from './components/chat-group-list/chat-group-list.component';
|
import { ChatGroupListComponent } from './components/chat-group-list/chat-group-list.component';
|
||||||
|
import { ChatMessageComponent } from './components/chat-message/chat-message.component';
|
||||||
import { ChatRoutingModule } from './chat-routing.module';
|
import { ChatRoutingModule } from './chat-routing.module';
|
||||||
|
import { ChatTabsComponent } from './components/chat-tabs/chat-tabs.component';
|
||||||
|
import { EditChatGroupDialogComponent } from './components/edit-chat-group-dialog/edit-chat-group-dialog.component';
|
||||||
import { SharedModule } from '../../shared/shared.module';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [CommonModule, ChatRoutingModule, SharedModule],
|
imports: [CommonModule, ChatRoutingModule, SharedModule],
|
||||||
declarations: [ChatGroupListComponent, ChatGroupDetailComponent]
|
declarations: [
|
||||||
|
ChatGroupListComponent,
|
||||||
|
ChatGroupDetailComponent,
|
||||||
|
ChatTabsComponent,
|
||||||
|
EditChatGroupDialogComponent,
|
||||||
|
ChatMessageComponent
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class ChatModule {}
|
export class ChatModule {}
|
||||||
|
@ -1,53 +1,45 @@
|
|||||||
<os-head-bar [nav]="false">
|
<div class="chat-header" *osPerms="permission.chatCanManage">
|
||||||
<!-- Title -->
|
<os-icon-container
|
||||||
<div *ngIf="chatgroup" class="title-slot">
|
icon="group"
|
||||||
<h2>{{ chatgroup.name }}</h2>
|
*ngIf="chatGroup.access_groups.length"
|
||||||
</div>
|
matTooltip="{{ 'Access groups' | translate }}"
|
||||||
|
>
|
||||||
<!-- Menu -->
|
<span *ngFor="let group of chatGroup.access_groups | slice: 0:3; let last = last">
|
||||||
<div class="menu-slot" *osPerms="'chat.can_manage'">
|
<span>{{ group.getTitle() | translate }}</span>
|
||||||
<button type="button" mat-icon-button [matMenuTriggerFor]="chatgroupMenu">
|
<span *ngIf="!last">, </span>
|
||||||
<mat-icon>more_vert</mat-icon>
|
<span *ngIf="last && chatGroup.access_groups.length > 3">...</span>
|
||||||
|
</span>
|
||||||
|
</os-icon-container>
|
||||||
|
<button class="chat-options" mat-icon-button [matMenuTriggerFor]="chatgroupMenu">
|
||||||
|
<mat-icon> more_vert </mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</os-head-bar>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="messages">
|
|
||||||
<div *ngFor="let message of chatMessages">
|
|
||||||
{{ message.username }} ({{ message.timestamp }}): {{ message.text }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-list-wrapper">
|
||||||
<div>
|
<cdk-virtual-scroll-viewport class="chat-message-list" itemSize="70">
|
||||||
<form [formGroup]="newMessageForm">
|
<div *cdkVirtualFor="let message of chatMessages" class="chat-message">
|
||||||
<mat-form-field>
|
<os-chat-message [message]="message" (deleteEvent)="deleteChatMessage(message)"></os-chat-message>
|
||||||
<input
|
</div>
|
||||||
type="text"
|
</cdk-virtual-scroll-viewport>
|
||||||
matInput
|
|
||||||
osAutofocus
|
|
||||||
required
|
|
||||||
formControlName="text"
|
|
||||||
/>
|
|
||||||
<mat-error *ngIf="newMessageForm.invalid">{{ 'Required' | translate }}</mat-error>
|
|
||||||
</mat-form-field>
|
|
||||||
</form>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
mat-button
|
|
||||||
[disabled]="!newMessageForm.valid"
|
|
||||||
color="accent"
|
|
||||||
(click)="send()"
|
|
||||||
>
|
|
||||||
<span>{{ 'Send' | translate }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- The menu content -->
|
<!-- The menu content -->
|
||||||
<mat-menu #chatgroupMenu="matMenu">
|
<mat-menu #chatgroupMenu="matMenu">
|
||||||
<button mat-menu-item (click)="clearChat()" class="red-warning-text">
|
<ng-container *osPerms="permission.chatCanManage">
|
||||||
<mat-icon>delete</mat-icon>
|
<button mat-menu-item (click)="editChat()">
|
||||||
<span>{{ 'Clear the chat' | translate }}</span>
|
<mat-icon>edit</mat-icon>
|
||||||
|
<span>{{ 'Edit chat' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- clear history -->
|
||||||
|
<button mat-menu-item (click)="clearChat()" class="red-warning-text">
|
||||||
|
<mat-icon>format_clear</mat-icon>
|
||||||
|
<span>{{ 'Clear chat' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
<!-- delete -->
|
||||||
|
<button mat-menu-item (click)="deleteChatGroup()" class="red-warning-text">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
<span>{{ 'Delete chat' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
<!-- edit -->
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
.chat-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.chat-options {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-list-wrapper {
|
||||||
|
min-height: 300px;
|
||||||
|
height: 50vh;
|
||||||
|
|
||||||
|
.chat-message-list {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message {
|
||||||
|
height: auto;
|
||||||
|
}
|
@ -1,79 +1,157 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { CdkVirtualScrollViewport, ExtendedScrollToOptions } from '@angular/cdk/scrolling';
|
||||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
ViewChild
|
||||||
|
} from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { ChatGroupRepositoryService } from 'app/core/repositories/chat/chat-group-repository.service';
|
import { ChatGroupRepositoryService } from 'app/core/repositories/chat/chat-group-repository.service';
|
||||||
import { ChatMessageRepositoryService } from 'app/core/repositories/chat/chat-message-repository.service';
|
import { ChatMessageRepositoryService } from 'app/core/repositories/chat/chat-message-repository.service';
|
||||||
import { ChatNotificationService } from 'app/core/ui-services/chat-notification.service';
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { ChatMessage } from 'app/shared/models/chat/chat-message';
|
import { ChatGroup } from 'app/shared/models/chat/chat-group';
|
||||||
|
import { infoDialogSettings } from 'app/shared/utils/dialog-settings';
|
||||||
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
||||||
|
import { ChatNotificationService } from 'app/site/chat/services/chat-notification.service';
|
||||||
|
import {
|
||||||
|
ChatGroupData,
|
||||||
|
EditChatGroupDialogComponent
|
||||||
|
} from '../edit-chat-group-dialog/edit-chat-group-dialog.component';
|
||||||
import { ViewChatGroup } from '../../models/view-chat-group';
|
import { ViewChatGroup } from '../../models/view-chat-group';
|
||||||
import { ViewChatMessage } from '../../models/view-chat-message';
|
import { ViewChatMessage } from '../../models/view-chat-message';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'os-chat-group-detail',
|
selector: 'os-chat-group-detail',
|
||||||
templateUrl: './chat-group-detail.component.html',
|
templateUrl: './chat-group-detail.component.html',
|
||||||
styleUrls: ['./chat-group-detail.component.scss']
|
styleUrls: ['./chat-group-detail.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class ChatGroupDetailComponent extends BaseViewComponentDirective implements OnInit, OnDestroy {
|
export class ChatGroupDetailComponent extends BaseViewComponentDirective implements OnInit, AfterViewInit, OnDestroy {
|
||||||
public newMessageForm: FormGroup;
|
@Input()
|
||||||
public chatgroup: ViewChatGroup;
|
public chatGroup: ViewChatGroup;
|
||||||
public chatgroupId: number;
|
|
||||||
|
@ViewChild(CdkVirtualScrollViewport)
|
||||||
|
public virtualScrollViewport?: CdkVirtualScrollViewport;
|
||||||
|
|
||||||
|
public chatGroupId: number;
|
||||||
|
|
||||||
public chatMessages: ViewChatMessage[] = [];
|
public chatMessages: ViewChatMessage[] = [];
|
||||||
|
|
||||||
|
public get isOnBottomOfChat(): boolean {
|
||||||
|
const isOnBottom = this.virtualScrollViewport?.measureScrollOffset('bottom') === 0;
|
||||||
|
return isOnBottom;
|
||||||
|
}
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
titleService: Title,
|
titleService: Title,
|
||||||
protected translate: TranslateService,
|
protected translate: TranslateService,
|
||||||
matSnackBar: MatSnackBar,
|
matSnackBar: MatSnackBar,
|
||||||
private chatGroupRepo: ChatGroupRepositoryService,
|
private repo: ChatGroupRepositoryService,
|
||||||
private chatMessageRepo: ChatMessageRepositoryService,
|
private chatMessageRepo: ChatMessageRepositoryService,
|
||||||
private route: ActivatedRoute,
|
private chatNotificationService: ChatNotificationService,
|
||||||
private formBuilder: FormBuilder,
|
private dialog: MatDialog,
|
||||||
private chatNotificationService: ChatNotificationService
|
private promptService: PromptService,
|
||||||
|
private cd: ChangeDetectorRef
|
||||||
) {
|
) {
|
||||||
super(titleService, translate, matSnackBar);
|
super(titleService, translate, matSnackBar);
|
||||||
|
}
|
||||||
|
|
||||||
this.newMessageForm = this.formBuilder.group({
|
public ngOnInit(): void {
|
||||||
text: ['', Validators.required]
|
this.chatGroupId = this.chatGroup.id;
|
||||||
|
this.chatNotificationService.openChat(this.chatGroupId);
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.chatMessageRepo.getViewModelListBehaviorSubject().subscribe(chatMessages => {
|
||||||
|
this.chatMessages = chatMessages.filter(message => {
|
||||||
|
return message.chatgroup_id === this.chatGroup.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.chatgroupId = parseInt(this.route.snapshot.params.id, 10);
|
if (this.isOnBottomOfChat) {
|
||||||
|
this.scrollToBottom();
|
||||||
this.subscriptions.push(
|
|
||||||
this.chatGroupRepo.getViewModelObservable(this.chatgroupId).subscribe(chatGroup => {
|
|
||||||
if (chatGroup) {
|
|
||||||
super.setTitle(`${this.translate.instant('Chat group')} - ${chatGroup.getTitle()}`);
|
|
||||||
this.chatgroup = chatGroup;
|
|
||||||
}
|
}
|
||||||
}),
|
this.cd.markForCheck();
|
||||||
this.chatMessageRepo.getViewModelListBehaviorSubject().subscribe(chatMessages => {
|
|
||||||
this.chatMessages = chatMessages.filter(message => message.chatgroup_id === this.chatgroup.id);
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit(): void {
|
public ngAfterViewInit(): void {
|
||||||
super.setTitle('Chat group');
|
this.scrollToBottom();
|
||||||
this.chatNotificationService.openChat(this.chatgroupId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public send(): void {
|
|
||||||
const payload = {
|
|
||||||
text: this.newMessageForm.value.text,
|
|
||||||
chatgroup_id: this.chatgroup.id
|
|
||||||
};
|
|
||||||
this.chatMessageRepo.create(payload as ChatMessage).catch(this.raiseError);
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearChat(): void {
|
|
||||||
this.chatGroupRepo.clearMessages(this.chatgroup).catch(this.raiseError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy(): void {
|
public ngOnDestroy(): void {
|
||||||
this.chatNotificationService.closeChat(this.chatgroupId);
|
this.chatNotificationService.closeChat(this.chatGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private scrollToBottom(): void {
|
||||||
|
/**
|
||||||
|
* I am aware that this is ugly, but that is the only way to get to
|
||||||
|
* the bottom reliably
|
||||||
|
* https://stackoverflow.com/questions/64932671/scroll-to-bottom-with-cdk-virtual-scroll-angular-8/65069130
|
||||||
|
*/
|
||||||
|
const scrollTarget: ExtendedScrollToOptions = {
|
||||||
|
bottom: 0,
|
||||||
|
behavior: 'auto'
|
||||||
|
};
|
||||||
|
setTimeout(() => {
|
||||||
|
this.virtualScrollViewport.scrollTo(scrollTarget);
|
||||||
|
}, 0);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.virtualScrollViewport.scrollTo(scrollTarget);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
public editChat(): void {
|
||||||
|
const chatData: ChatGroupData = {
|
||||||
|
name: this.chatGroup.name,
|
||||||
|
access_groups_id: this.chatGroup.access_groups_id
|
||||||
|
};
|
||||||
|
|
||||||
|
const dialogRef = this.dialog.open(EditChatGroupDialogComponent, {
|
||||||
|
data: chatData,
|
||||||
|
...infoDialogSettings
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe((res: ChatGroupData) => {
|
||||||
|
if (res) {
|
||||||
|
this.save(res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async save(chatData: ChatGroupData): Promise<void> {
|
||||||
|
await this.repo.update(chatData as ChatGroup, this.chatGroup).catch(this.raiseError);
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async clearChat(): Promise<void> {
|
||||||
|
const title = this.translate.instant('Are you sure you want to clear this chats history?');
|
||||||
|
if (await this.promptService.open(title)) {
|
||||||
|
await this.repo.clearMessages(this.chatGroup).catch(this.raiseError);
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteChatGroup(): Promise<void> {
|
||||||
|
const title = this.translate.instant('Are you sure you want to delete this chat?');
|
||||||
|
if (await this.promptService.open(title)) {
|
||||||
|
await this.repo.delete(this.chatGroup).catch(this.raiseError);
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteChatMessage(message: ViewChatMessage): Promise<void> {
|
||||||
|
const title = this.translate.instant('Are you sure you want to delete this message?');
|
||||||
|
if (await this.promptService.open(title)) {
|
||||||
|
await this.chatMessageRepo.delete(message).catch(this.raiseError);
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,60 +1,10 @@
|
|||||||
<os-head-bar [hasMainButton]="canEdit" (mainEvent)="createNewChatGroup()">
|
<os-head-bar [hasMainButton]="canManage" (mainEvent)="createNewChatGroup()">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="title-slot">
|
<div class="title-slot">
|
||||||
<h2>{{ 'Chat groups' | translate }}</h2>
|
<h2>{{ 'Chat' | translate }}</h2>
|
||||||
</div>
|
</div>
|
||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
|
||||||
<div *ngFor="let chatGroup of chatGroups">
|
<mat-card class="os-card spacer-bottom-340">
|
||||||
<a [routerLink]="'./' + chatGroup.id">{{ chatGroup.name }}</a><br/>
|
<os-chat-tabs></os-chat-tabs>
|
||||||
{{ chatGroup.access_groups.length ? chatGroup.access_groups : 'No access groups, public to all' }}<br/>
|
</mat-card>
|
||||||
<button type="button" *osPerms="'chat.can_manage'" mat-icon-button (click)="edit(chatGroup)">
|
|
||||||
<mat-icon>create</mat-icon>
|
|
||||||
</button>
|
|
||||||
<div *ngIf="amountNotification(chatGroup)>0">{{ amountNotification(chatGroup) }} NEW MESSAGES</div>
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- edit/update dialog -->
|
|
||||||
<ng-template #createUpdateDialog>
|
|
||||||
<h1 *ngIf="isCreateMode" mat-dialog-title>{{ 'Create new chat group' | translate }}</h1>
|
|
||||||
<h1 *ngIf="isEditMode" mat-dialog-title>{{ 'Edit details for' | translate }} {{ editModel.name }}</h1>
|
|
||||||
<div class="os-form-card-mobile" mat-dialog-content>
|
|
||||||
<form [formGroup]="createUpdateForm">
|
|
||||||
<mat-form-field>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
matInput
|
|
||||||
osAutofocus
|
|
||||||
required
|
|
||||||
formControlName="name"
|
|
||||||
placeholder="{{ 'Name' | translate }}"
|
|
||||||
/>
|
|
||||||
<mat-error *ngIf="createUpdateForm.invalid">{{ 'Required' | translate }}</mat-error>
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<mat-form-field>
|
|
||||||
<os-search-value-selector
|
|
||||||
formControlName="access_groups_id"
|
|
||||||
[multiple]="true"
|
|
||||||
placeholder="{{ 'Access groups' | translate }}"
|
|
||||||
[inputListValues]="groupsBehaviorSubject"
|
|
||||||
></os-search-value-selector>
|
|
||||||
</mat-form-field>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div mat-dialog-actions>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
mat-button
|
|
||||||
[disabled]="!createUpdateForm.valid"
|
|
||||||
[mat-dialog-close]="true"
|
|
||||||
color="accent"
|
|
||||||
>
|
|
||||||
<span>{{ 'Save' | translate }}</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" mat-button [mat-dialog-close]="false">
|
|
||||||
<span>{{ 'Cancel' | translate }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { BehaviorSubject } from 'rxjs';
|
|
||||||
|
|
||||||
import { OperatorService, Permission } from 'app/core/core-services/operator.service';
|
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||||
import { ChatGroupRepositoryService } from 'app/core/repositories/chat/chat-group-repository.service';
|
import { ChatGroupRepositoryService } from 'app/core/repositories/chat/chat-group-repository.service';
|
||||||
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
|
|
||||||
import { ChatNotificationService, NotificationAmount } from 'app/core/ui-services/chat-notification.service';
|
|
||||||
import { ChatGroup } from 'app/shared/models/chat/chat-group';
|
import { ChatGroup } from 'app/shared/models/chat/chat-group';
|
||||||
|
import { infoDialogSettings } from 'app/shared/utils/dialog-settings';
|
||||||
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
||||||
import { ViewGroup } from 'app/site/users/models/view-group';
|
import { ChatNotificationService, NotificationAmount } from 'app/site/chat/services/chat-notification.service';
|
||||||
|
import {
|
||||||
|
ChatGroupData,
|
||||||
|
EditChatGroupDialogComponent
|
||||||
|
} from '../edit-chat-group-dialog/edit-chat-group-dialog.component';
|
||||||
import { ViewChatGroup } from '../../models/view-chat-group';
|
import { ViewChatGroup } from '../../models/view-chat-group';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -22,99 +23,39 @@ import { ViewChatGroup } from '../../models/view-chat-group';
|
|||||||
styleUrls: ['./chat-group-list.component.scss']
|
styleUrls: ['./chat-group-list.component.scss']
|
||||||
})
|
})
|
||||||
export class ChatGroupListComponent extends BaseViewComponentDirective implements OnInit {
|
export class ChatGroupListComponent extends BaseViewComponentDirective implements OnInit {
|
||||||
@ViewChild('createUpdateDialog', { static: true })
|
public get canManage(): boolean {
|
||||||
private createUpdateDialog: TemplateRef<string>;
|
return this.operator.hasPerms(this.permission.chatCanManage);
|
||||||
|
|
||||||
public createUpdateForm: FormGroup;
|
|
||||||
public groupsBehaviorSubject: BehaviorSubject<ViewGroup[]>;
|
|
||||||
|
|
||||||
private isEdit = false;
|
|
||||||
public editModel: ViewChatGroup | null = null;
|
|
||||||
|
|
||||||
public get isCreateMode(): boolean {
|
|
||||||
return this.isEdit && this.editModel === null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isEditMode(): boolean {
|
|
||||||
return this.isEdit && this.editModel !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public chatGroups: ViewChatGroup[] = [];
|
|
||||||
|
|
||||||
public get canEdit(): boolean {
|
|
||||||
return this.operator.hasPerms(Permission.chatCanManage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public notificationAmounts: NotificationAmount = {};
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
titleService: Title,
|
titleService: Title,
|
||||||
protected translate: TranslateService,
|
protected translate: TranslateService,
|
||||||
matSnackBar: MatSnackBar,
|
matSnackBar: MatSnackBar,
|
||||||
private repo: ChatGroupRepositoryService,
|
private repo: ChatGroupRepositoryService,
|
||||||
private formBuilder: FormBuilder,
|
|
||||||
private groupRepo: GroupRepositoryService,
|
|
||||||
private dialog: MatDialog,
|
private dialog: MatDialog,
|
||||||
private operator: OperatorService,
|
private operator: OperatorService
|
||||||
private chatNotificationService: ChatNotificationService
|
|
||||||
) {
|
) {
|
||||||
super(titleService, translate, matSnackBar);
|
super(titleService, translate, matSnackBar);
|
||||||
this.groupsBehaviorSubject = this.groupRepo.getViewModelListBehaviorSubject();
|
|
||||||
|
|
||||||
this.repo.getViewModelListBehaviorSubject().subscribe(list => (this.chatGroups = list));
|
|
||||||
this.chatNotificationService.chatgroupNotificationsObservable.subscribe(n => (this.notificationAmounts = n));
|
|
||||||
|
|
||||||
this.createUpdateForm = this.formBuilder.group({
|
|
||||||
name: ['', Validators.required],
|
|
||||||
access_groups_id: [[]]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
super.setTitle('Chat groups');
|
super.setTitle('Chat');
|
||||||
}
|
}
|
||||||
|
|
||||||
public createNewChatGroup(): void {
|
public createNewChatGroup(): void {
|
||||||
if (this.isEdit) {
|
const dialogRef = this.dialog.open(EditChatGroupDialogComponent, {
|
||||||
return;
|
data: null,
|
||||||
}
|
...infoDialogSettings
|
||||||
|
});
|
||||||
|
|
||||||
this.isEdit = true;
|
dialogRef.afterClosed().subscribe((res: ChatGroupData) => {
|
||||||
this.editModel = null;
|
|
||||||
this.createUpdateForm.reset();
|
|
||||||
this.openDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
public edit(chatGroup: ViewChatGroup): void {
|
|
||||||
if (this.isEdit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isEdit = true;
|
|
||||||
this.editModel = chatGroup;
|
|
||||||
this.createUpdateForm.patchValue({ name: chatGroup.name, access_groups_id: chatGroup.access_groups_id });
|
|
||||||
this.openDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
private openDialog(): void {
|
|
||||||
const dialogRef = this.dialog.open(this.createUpdateDialog);
|
|
||||||
dialogRef.afterClosed().subscribe(res => {
|
|
||||||
if (res) {
|
if (res) {
|
||||||
this.save();
|
this.save(res);
|
||||||
}
|
}
|
||||||
this.isEdit = false;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public save(): void {
|
public save(createData: ChatGroupData): void {
|
||||||
if (this.isCreateMode) {
|
this.repo.create(createData as ChatGroup).catch(this.raiseError);
|
||||||
this.repo.create(this.createUpdateForm.value as ChatGroup).catch(this.raiseError);
|
|
||||||
} else if (this.isEditMode) {
|
|
||||||
this.repo.update(this.createUpdateForm.value as ChatGroup, this.editModel).catch(this.raiseError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public amountNotification(chatGroup: ViewChatGroup): number {
|
|
||||||
return this.notificationAmounts[chatGroup.id];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
<div class="message-box" [ngClass]="{ 'incomming-message': !isOwnMessage, 'outgoind-message': isOwnMessage }">
|
||||||
|
<div class="author" *ngIf="!isOwnMessage">
|
||||||
|
{{ author }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="chat-text"
|
||||||
|
[ngClass]="{ 'background-primary': !isOwnMessage, 'background-primary-darkest': isOwnMessage }"
|
||||||
|
[matMenuTriggerFor]="canDelete ? singleChatMenu : null"
|
||||||
|
>
|
||||||
|
{{ text }}
|
||||||
|
<div class="timestamp">{{ date | localizedDate }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-menu #singleChatMenu="matMenu">
|
||||||
|
<!-- Delete this message -->
|
||||||
|
<button mat-menu-item (click)="onDeleteMessage()" *ngIf="canDelete">
|
||||||
|
<mat-icon color="warn">delete</mat-icon>
|
||||||
|
<span>{{ 'Delete' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
@ -0,0 +1,37 @@
|
|||||||
|
$message-box-radius: 20px;
|
||||||
|
|
||||||
|
.incomming-message {
|
||||||
|
.chat-text {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.outgoind-message {
|
||||||
|
.chat-text {
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-box {
|
||||||
|
margin: 1em 0.5em 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-text {
|
||||||
|
padding: 0.5em;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 90%;
|
||||||
|
border-radius: $message-box-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
font-size: small;
|
||||||
|
opacity: 0.5;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { ChatMessageComponent } from './chat-message.component';
|
||||||
|
|
||||||
|
describe('ChatMessageComponent', () => {
|
||||||
|
let component: ChatMessageComponent;
|
||||||
|
let fixture: ComponentFixture<ChatMessageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule],
|
||||||
|
declarations: [ChatMessageComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ChatMessageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,41 @@
|
|||||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
|
||||||
|
import { OperatorService, Permission } from 'app/core/core-services/operator.service';
|
||||||
|
import { ViewChatMessage } from '../../models/view-chat-message';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'os-chat-message',
|
||||||
|
templateUrl: './chat-message.component.html',
|
||||||
|
styleUrls: ['./chat-message.component.scss']
|
||||||
|
})
|
||||||
|
export class ChatMessageComponent {
|
||||||
|
@Input() private message: ViewChatMessage;
|
||||||
|
|
||||||
|
@Output() public deleteEvent = new EventEmitter();
|
||||||
|
|
||||||
|
public get isOwnMessage(): boolean {
|
||||||
|
return this.operator?.user?.id === this.message?.user_id || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get canDelete(): boolean {
|
||||||
|
return this.isOwnMessage || this.operator.hasPerms(Permission.chatCanManage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get author(): string {
|
||||||
|
return this.message?.username || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public get text(): string {
|
||||||
|
return this.message?.text || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public get date(): Date {
|
||||||
|
return this.message?.timestampAsDate || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(private operator: OperatorService) {}
|
||||||
|
|
||||||
|
public onDeleteMessage(): void {
|
||||||
|
this.deleteEvent.next();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
<mat-tab-group (selectedTabChange)="selectedTabChange($event)" *ngIf="chatGroupsExist()">
|
||||||
|
<mat-tab *ngFor="let chat of chatGroupSubject | async">
|
||||||
|
<ng-template mat-tab-label>
|
||||||
|
<span
|
||||||
|
[matBadgeHidden]="!getNotidficationsForChatId(chat.id)"
|
||||||
|
[matBadge]="getNotidficationsForChatId(chat.id)"
|
||||||
|
matBadgeColor="accent"
|
||||||
|
matBadgeOverlap="false"
|
||||||
|
>
|
||||||
|
{{ chat.name }}
|
||||||
|
</span>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template matTabContent>
|
||||||
|
<os-chat-group-detail [chatGroup]="chat"> </os-chat-group-detail>
|
||||||
|
</ng-template>
|
||||||
|
</mat-tab>
|
||||||
|
</mat-tab-group>
|
||||||
|
|
||||||
|
<div *ngIf="!chatGroupsExist()">
|
||||||
|
<span>
|
||||||
|
{{ 'No chat groups available' | translate }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- send chat -->
|
||||||
|
<form [formGroup]="newMessageForm" (ngSubmit)="send()" *ngIf="chatGroupsExist()">
|
||||||
|
<mat-form-field appearance="outline" class="chat-form-field">
|
||||||
|
<input class="chat-input" type="text" matInput formControlName="text" />
|
||||||
|
<mat-label>{{ 'Message' | translate }}</mat-label>
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
matSuffix
|
||||||
|
type="submit"
|
||||||
|
color="accent"
|
||||||
|
matTooltip=" {{ 'Send' | translate }}"
|
||||||
|
[disabled]="isChatMessageEmpty()"
|
||||||
|
>
|
||||||
|
<mat-icon>send</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-form-field>
|
||||||
|
</form>
|
@ -0,0 +1,7 @@
|
|||||||
|
.chat-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input {
|
||||||
|
// todo
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { ChatTabsComponent } from './chat-tabs.component';
|
||||||
|
|
||||||
|
describe('ChatTabsComponent', () => {
|
||||||
|
let component: ChatTabsComponent;
|
||||||
|
let fixture: ComponentFixture<ChatTabsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ChatTabsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,86 @@
|
|||||||
|
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
import { MatTabChangeEvent } from '@angular/material/tabs';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { ChatGroupRepositoryService } from 'app/core/repositories/chat/chat-group-repository.service';
|
||||||
|
import { ChatMessageRepositoryService } from 'app/core/repositories/chat/chat-message-repository.service';
|
||||||
|
import { ChatMessage } from 'app/shared/models/chat/chat-message';
|
||||||
|
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
||||||
|
import { ChatNotificationService, NotificationAmount } from '../../services/chat-notification.service';
|
||||||
|
import { ViewChatGroup } from '../../models/view-chat-group';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'os-chat-tabs',
|
||||||
|
templateUrl: './chat-tabs.component.html',
|
||||||
|
styleUrls: ['./chat-tabs.component.scss'],
|
||||||
|
encapsulation: ViewEncapsulation.None
|
||||||
|
})
|
||||||
|
export class ChatTabsComponent extends BaseViewComponentDirective implements OnInit {
|
||||||
|
public chatGroupSubject: BehaviorSubject<ViewChatGroup[]>;
|
||||||
|
public newMessageForm: FormGroup;
|
||||||
|
private selectedTabIndex = 0;
|
||||||
|
|
||||||
|
private notifications: NotificationAmount;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
titleService: Title,
|
||||||
|
translate: TranslateService,
|
||||||
|
matSnackBar: MatSnackBar,
|
||||||
|
private repo: ChatGroupRepositoryService,
|
||||||
|
private chatMessageRepo: ChatMessageRepositoryService,
|
||||||
|
private chatNotificationService: ChatNotificationService,
|
||||||
|
formBuilder: FormBuilder
|
||||||
|
) {
|
||||||
|
super(titleService, translate, matSnackBar);
|
||||||
|
|
||||||
|
this.newMessageForm = formBuilder.group({
|
||||||
|
text: ['']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.chatGroupSubject = this.repo.getViewModelListBehaviorSubject();
|
||||||
|
|
||||||
|
this.chatNotificationService.chatgroupNotificationsObservable.subscribe(notifications => {
|
||||||
|
this.notifications = notifications;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectedTabChange(event: MatTabChangeEvent): void {
|
||||||
|
this.selectedTabIndex = event.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getNotidficationsForChatId(chatId: number): number {
|
||||||
|
return this.notifications?.[chatId] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public chatGroupsExist(): boolean {
|
||||||
|
return this.chatGroupSubject.value.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isChatMessageEmpty(): boolean {
|
||||||
|
return !this.newMessageForm?.value?.text?.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public send(): void {
|
||||||
|
const payload = {
|
||||||
|
text: this.newMessageForm.value.text,
|
||||||
|
chatgroup_id: this.chatGroupSubject.value[this.selectedTabIndex].id
|
||||||
|
};
|
||||||
|
this.chatMessageRepo
|
||||||
|
.create(payload as ChatMessage)
|
||||||
|
.then(() => {
|
||||||
|
this.clearTextInput();
|
||||||
|
})
|
||||||
|
.catch(this.raiseError);
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearTextInput(): void {
|
||||||
|
this.newMessageForm.reset();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
<h1 mat-dialog-title>
|
||||||
|
<span *ngIf="createMode">
|
||||||
|
{{ 'Create new chat group' | translate }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span *ngIf="!createMode"> {{ 'Edit details for' | translate }} {{ previousChatGroupName }} </span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<form [formGroup]="createUpdateForm">
|
||||||
|
<div class="os-form-card-mobile" mat-dialog-content>
|
||||||
|
<!-- Name -->
|
||||||
|
<mat-form-field>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
matInput
|
||||||
|
osAutofocus
|
||||||
|
required
|
||||||
|
formControlName="name"
|
||||||
|
placeholder="{{ 'Name' | translate }}"
|
||||||
|
/>
|
||||||
|
<mat-error *ngIf="createUpdateForm.invalid">{{ 'Required' | translate }}</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<!-- Groups -->
|
||||||
|
<mat-form-field>
|
||||||
|
<os-search-value-selector
|
||||||
|
formControlName="access_groups_id"
|
||||||
|
[multiple]="true"
|
||||||
|
placeholder="{{ 'Access groups' | translate }}"
|
||||||
|
[inputListValues]="groupsBehaviorSubject"
|
||||||
|
></os-search-value-selector>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<!-- save -->
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
color="accent"
|
||||||
|
mat-button
|
||||||
|
[disabled]="createUpdateForm.invalid"
|
||||||
|
[mat-dialog-close]="createUpdateForm.value"
|
||||||
|
color="accent"
|
||||||
|
>
|
||||||
|
<span>{{ 'Save' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- cancel -->
|
||||||
|
<button type="button" mat-button [mat-dialog-close]="false">
|
||||||
|
<span>{{ 'Cancel' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -0,0 +1,35 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { EditChatGroupDialogComponent } from './edit-chat-group-dialog.component';
|
||||||
|
|
||||||
|
describe('EditChatGroupDialogComponent', () => {
|
||||||
|
let component: EditChatGroupDialogComponent;
|
||||||
|
let fixture: ComponentFixture<EditChatGroupDialogComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule],
|
||||||
|
providers: [
|
||||||
|
{ provide: MatDialogRef, useValue: {} },
|
||||||
|
{
|
||||||
|
provide: MAT_DIALOG_DATA,
|
||||||
|
useValue: null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
declarations: [EditChatGroupDialogComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(EditChatGroupDialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,43 @@
|
|||||||
|
import { Component, Inject } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
|
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
|
||||||
|
import { ViewGroup } from 'app/site/users/models/view-group';
|
||||||
|
|
||||||
|
export interface ChatGroupData {
|
||||||
|
name: string;
|
||||||
|
access_groups_id: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'os-edit-chat-group-dialog',
|
||||||
|
templateUrl: './edit-chat-group-dialog.component.html',
|
||||||
|
styleUrls: ['./edit-chat-group-dialog.component.scss']
|
||||||
|
})
|
||||||
|
export class EditChatGroupDialogComponent {
|
||||||
|
public createUpdateForm: FormGroup;
|
||||||
|
public groupsBehaviorSubject: BehaviorSubject<ViewGroup[]>;
|
||||||
|
|
||||||
|
public createMode: boolean;
|
||||||
|
|
||||||
|
public get previousChatGroupName(): string {
|
||||||
|
return this.data?.name || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
groupRepo: GroupRepositoryService,
|
||||||
|
formBuilder: FormBuilder,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data?: ChatGroupData
|
||||||
|
) {
|
||||||
|
this.createMode = !data;
|
||||||
|
this.createUpdateForm = formBuilder.group({
|
||||||
|
name: [data?.name || '', Validators.required],
|
||||||
|
access_groups_id: [data?.access_groups_id || []]
|
||||||
|
});
|
||||||
|
this.groupsBehaviorSubject = groupRepo.getViewModelListBehaviorSubject();
|
||||||
|
}
|
||||||
|
}
|
@ -3,8 +3,8 @@ import { Injectable } from '@angular/core';
|
|||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
|
||||||
import { ViewChatMessage } from 'app/site/chat/models/view-chat-message';
|
import { ViewChatMessage } from 'app/site/chat/models/view-chat-message';
|
||||||
import { ChatMessageRepositoryService } from '../repositories/chat/chat-message-repository.service';
|
import { ChatMessageRepositoryService } from '../../../core/repositories/chat/chat-message-repository.service';
|
||||||
import { StorageService } from '../core-services/storage.service';
|
import { StorageService } from '../../../core/core-services/storage.service';
|
||||||
|
|
||||||
interface LastMessageTimestampsSeen {
|
interface LastMessageTimestampsSeen {
|
||||||
[chatgroupId: number]: Date;
|
[chatgroupId: number]: Date;
|
@ -2,9 +2,9 @@ import { Injectable } from '@angular/core';
|
|||||||
|
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
|
||||||
import { ChatGroupRepositoryService } from '../repositories/chat/chat-group-repository.service';
|
import { ChatGroupRepositoryService } from '../../../core/repositories/chat/chat-group-repository.service';
|
||||||
import { ConstantsService } from '../core-services/constants.service';
|
import { ConstantsService } from '../../../core/core-services/constants.service';
|
||||||
import { OperatorService, Permission } from '../core-services/operator.service';
|
import { OperatorService, Permission } from '../../../core/core-services/operator.service';
|
||||||
|
|
||||||
interface OpenSlidesSettings {
|
interface OpenSlidesSettings {
|
||||||
ENABLE_CHAT: boolean;
|
ENABLE_CHAT: boolean;
|
@ -48,10 +48,16 @@
|
|||||||
routerLink="/chat"
|
routerLink="/chat"
|
||||||
routerLinkActive="active"
|
routerLinkActive="active"
|
||||||
>
|
>
|
||||||
<mat-icon *ngIf="chatNotificationAmount <= 0">chat</mat-icon>
|
<mat-icon
|
||||||
<mat-icon *ngIf="chatNotificationAmount > 0" class="unread-chat">notification_important</mat-icon>
|
[matBadgeHidden]="chatNotificationAmount === 0"
|
||||||
<span>{{ 'Chat' | translate }}</span>
|
[matBadge]="chatNotificationAmount"
|
||||||
<span *ngIf="chatNotificationAmount > 0">({{ chatNotificationAmount }})</span>
|
matBadgeColor="accent"
|
||||||
|
matBadgeOverlap="false"
|
||||||
|
>chat</mat-icon
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{ 'Chat' | translate }}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
[@navItemAnim]
|
[@navItemAnim]
|
||||||
|
@ -11,10 +11,10 @@ import { filter } from 'rxjs/operators';
|
|||||||
|
|
||||||
import { navItemAnim } from '../shared/animations';
|
import { navItemAnim } from '../shared/animations';
|
||||||
import { OfflineBroadcastService } from 'app/core/core-services/offline-broadcast.service';
|
import { OfflineBroadcastService } from 'app/core/core-services/offline-broadcast.service';
|
||||||
import { ChatNotificationService } from 'app/core/ui-services/chat-notification.service';
|
|
||||||
import { ChatService } from 'app/core/ui-services/chat.service';
|
|
||||||
import { OverlayService } from 'app/core/ui-services/overlay.service';
|
import { OverlayService } from 'app/core/ui-services/overlay.service';
|
||||||
import { UpdateService } from 'app/core/ui-services/update.service';
|
import { UpdateService } from 'app/core/ui-services/update.service';
|
||||||
|
import { ChatNotificationService } from 'app/site/chat/services/chat-notification.service';
|
||||||
|
import { ChatService } from 'app/site/chat/services/chat.service';
|
||||||
import { BaseComponent } from '../base.component';
|
import { BaseComponent } from '../base.component';
|
||||||
import { MainMenuEntry, MainMenuService } from '../core/core-services/main-menu.service';
|
import { MainMenuEntry, MainMenuService } from '../core/core-services/main-menu.service';
|
||||||
import { OpenSlidesStatusService } from '../core/core-services/openslides-status.service';
|
import { OpenSlidesStatusService } from '../core/core-services/openslides-status.service';
|
||||||
|
@ -173,10 +173,25 @@
|
|||||||
background-color: rgba(0, 0, 0, 0.025);
|
background-color: rgba(0, 0, 0, 0.025);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pbl-ngrid-header-row, .pbl-ngrid-row {
|
.pbl-ngrid-header-row,
|
||||||
|
.pbl-ngrid-row {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLEANUP:
|
||||||
|
* whole themes can be replaced using classes like this one
|
||||||
|
*/
|
||||||
|
.background-primary {
|
||||||
|
background: mat-color($primary);
|
||||||
|
color: mat-color($primary, default-contrast) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-primary-darkest {
|
||||||
|
background: mat-color($primary, 900);
|
||||||
|
color: mat-color($primary, default-contrast) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.primary-foreground {
|
.primary-foreground {
|
||||||
color: mat-color($primary);
|
color: mat-color($primary);
|
||||||
}
|
}
|
||||||
|
@ -462,6 +462,10 @@ button.mat-menu-item.selected {
|
|||||||
margin-bottom: 60px !important;
|
margin-bottom: 60px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spacer-bottom-340 {
|
||||||
|
margin-bottom: 340px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.spacer-left-10 {
|
.spacer-left-10 {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,7 @@ POSTFIX_RELAYHOST=
|
|||||||
# Features
|
# Features
|
||||||
ENABLE_SAML=
|
ENABLE_SAML=
|
||||||
ENABLE_ELECTRONIC_VOTING=
|
ENABLE_ELECTRONIC_VOTING=
|
||||||
|
ENABLE_CHAT=
|
||||||
DEMO_USERS=
|
DEMO_USERS=
|
||||||
|
|
||||||
# Connections
|
# Connections
|
||||||
|
@ -70,6 +70,7 @@ x-osserver-env: &default-osserver-env
|
|||||||
EMAIL_USE_TLS: "ifenvelse(`EMAIL_USE_TLS',)"
|
EMAIL_USE_TLS: "ifenvelse(`EMAIL_USE_TLS',)"
|
||||||
EMAIL_TIMEOUT: "ifenvelse(`EMAIL_TIMEOUT',)"
|
EMAIL_TIMEOUT: "ifenvelse(`EMAIL_TIMEOUT',)"
|
||||||
ENABLE_ELECTRONIC_VOTING: "ifenvelse(`ENABLE_ELECTRONIC_VOTING', False)"
|
ENABLE_ELECTRONIC_VOTING: "ifenvelse(`ENABLE_ELECTRONIC_VOTING', False)"
|
||||||
|
ENABLE_CHAT: "ifenvelse(`ENABLE_CHAT', False)"
|
||||||
ENABLE_SAML: "ifenvelse(`ENABLE_SAML', False)"
|
ENABLE_SAML: "ifenvelse(`ENABLE_SAML', False)"
|
||||||
INSTANCE_DOMAIN: "ifenvelse(`INSTANCE_DOMAIN', http://example.com:8000)"
|
INSTANCE_DOMAIN: "ifenvelse(`INSTANCE_DOMAIN', http://example.com:8000)"
|
||||||
JITSI_DOMAIN: "ifenvelse(`JITSI_DOMAIN',)"
|
JITSI_DOMAIN: "ifenvelse(`JITSI_DOMAIN',)"
|
||||||
|
@ -69,6 +69,7 @@ x-osserver-env: &default-osserver-env
|
|||||||
EMAIL_USE_TLS: "ifenvelse(`EMAIL_USE_TLS',)"
|
EMAIL_USE_TLS: "ifenvelse(`EMAIL_USE_TLS',)"
|
||||||
EMAIL_TIMEOUT: "ifenvelse(`EMAIL_TIMEOUT',)"
|
EMAIL_TIMEOUT: "ifenvelse(`EMAIL_TIMEOUT',)"
|
||||||
ENABLE_ELECTRONIC_VOTING: "ifenvelse(`ENABLE_ELECTRONIC_VOTING', False)"
|
ENABLE_ELECTRONIC_VOTING: "ifenvelse(`ENABLE_ELECTRONIC_VOTING', False)"
|
||||||
|
ENABLE_CHAT: "ifenvelse(`ENABLE_CHAT', False)"
|
||||||
ENABLE_SAML: "ifenvelse(`ENABLE_SAML', False)"
|
ENABLE_SAML: "ifenvelse(`ENABLE_SAML', False)"
|
||||||
INSTANCE_DOMAIN: "ifenvelse(`INSTANCE_DOMAIN', http://example.com:8000)"
|
INSTANCE_DOMAIN: "ifenvelse(`INSTANCE_DOMAIN', http://example.com:8000)"
|
||||||
JITSI_DOMAIN: "ifenvelse(`JITSI_DOMAIN',)"
|
JITSI_DOMAIN: "ifenvelse(`JITSI_DOMAIN',)"
|
||||||
|
@ -65,6 +65,13 @@ To enable it, set::
|
|||||||
|
|
||||||
ENABLE_ELECTRONIC_VOTING = True
|
ENABLE_ELECTRONIC_VOTING = True
|
||||||
|
|
||||||
|
Chat
|
||||||
|
=================
|
||||||
|
|
||||||
|
Is disabled by default. Enable the chatting feature::
|
||||||
|
|
||||||
|
ENABLE_CHAT = True
|
||||||
|
|
||||||
|
|
||||||
Jitsi integration
|
Jitsi integration
|
||||||
=================
|
=================
|
||||||
|
@ -125,6 +125,9 @@ if ENABLE_SAML:
|
|||||||
# Controls if electronic voting (means non-analog polls) are enabled.
|
# Controls if electronic voting (means non-analog polls) are enabled.
|
||||||
ENABLE_ELECTRONIC_VOTING = get_env("ENABLE_ELECTRONIC_VOTING", False, bool)
|
ENABLE_ELECTRONIC_VOTING = get_env("ENABLE_ELECTRONIC_VOTING", False, bool)
|
||||||
|
|
||||||
|
# Enable Chat
|
||||||
|
ENABLE_CHAT = get_env("ENABLE_CHAT", False, bool)
|
||||||
|
|
||||||
# Jitsi integration
|
# Jitsi integration
|
||||||
JITSI_DOMAIN = get_env("JITSI_DOMAIN", None)
|
JITSI_DOMAIN = get_env("JITSI_DOMAIN", None)
|
||||||
JITSI_ROOM_NAME = get_env("JITSI_ROOM_NAME", None)
|
JITSI_ROOM_NAME = get_env("JITSI_ROOM_NAME", None)
|
||||||
|
@ -55,6 +55,7 @@ class Migration(migrations.Migration):
|
|||||||
("text", models.CharField(max_length=512)),
|
("text", models.CharField(max_length=512)),
|
||||||
("timestamp", models.DateTimeField(auto_now_add=True)),
|
("timestamp", models.DateTimeField(auto_now_add=True)),
|
||||||
("username", models.CharField(max_length=256)),
|
("username", models.CharField(max_length=256)),
|
||||||
|
("user_id", models.IntegerField()),
|
||||||
(
|
(
|
||||||
"chatgroup",
|
"chatgroup",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
|
@ -78,6 +78,7 @@ class ChatMessage(RESTModelMixin, models.Model):
|
|||||||
ChatGroup, on_delete=CASCADE_AND_AUTOUPDATE, related_name="messages"
|
ChatGroup, on_delete=CASCADE_AND_AUTOUPDATE, related_name="messages"
|
||||||
)
|
)
|
||||||
username = models.CharField(max_length=256)
|
username = models.CharField(max_length=256)
|
||||||
|
user_id = models.IntegerField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
|
@ -45,9 +45,13 @@ class ChatMessageSerializer(ModelSerializer):
|
|||||||
"chatgroup",
|
"chatgroup",
|
||||||
"timestamp",
|
"timestamp",
|
||||||
"username",
|
"username",
|
||||||
|
"user_id",
|
||||||
"access_groups_id",
|
"access_groups_id",
|
||||||
)
|
)
|
||||||
read_only_fields = ("username",)
|
read_only_fields = (
|
||||||
|
"username",
|
||||||
|
"user_id",
|
||||||
|
)
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
if "text" in data:
|
if "text" in data:
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from rest_framework.utils.serializer_helpers import ReturnDict
|
from rest_framework.utils.serializer_helpers import ReturnDict
|
||||||
|
|
||||||
from openslides.utils.auth import has_perm
|
from openslides.utils.auth import has_perm
|
||||||
from openslides.utils.autoupdate import inform_changed_data, inform_deleted_data
|
from openslides.utils.autoupdate import (
|
||||||
|
disable_history,
|
||||||
|
inform_changed_data,
|
||||||
|
inform_deleted_data,
|
||||||
|
)
|
||||||
from openslides.utils.rest_api import (
|
from openslides.utils.rest_api import (
|
||||||
CreateModelMixin,
|
CreateModelMixin,
|
||||||
|
DestroyModelMixin,
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
ListModelMixin,
|
ListModelMixin,
|
||||||
ModelViewSet,
|
ModelViewSet,
|
||||||
@ -64,7 +70,11 @@ class ChatGroupViewSet(ModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class ChatMessageViewSet(
|
class ChatMessageViewSet(
|
||||||
ListModelMixin, RetrieveModelMixin, CreateModelMixin, GenericViewSet
|
ListModelMixin,
|
||||||
|
RetrieveModelMixin,
|
||||||
|
CreateModelMixin,
|
||||||
|
DestroyModelMixin,
|
||||||
|
GenericViewSet,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
API endpoint for chat groups.
|
API endpoint for chat groups.
|
||||||
@ -76,7 +86,8 @@ class ChatMessageViewSet(
|
|||||||
queryset = ChatMessage.objects.all()
|
queryset = ChatMessage.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
return ENABLE_CHAT
|
# The permissions are checked in the view.
|
||||||
|
return ENABLE_CHAT and not isinstance(self.request.user, AnonymousUser)
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
@ -89,6 +100,7 @@ class ChatMessageViewSet(
|
|||||||
validated_data = {
|
validated_data = {
|
||||||
**serializer.validated_data,
|
**serializer.validated_data,
|
||||||
"username": self.request.user.short_name(),
|
"username": self.request.user.short_name(),
|
||||||
|
"user_id": self.request.user.id,
|
||||||
}
|
}
|
||||||
chatmessage = ChatMessage(**validated_data)
|
chatmessage = ChatMessage(**validated_data)
|
||||||
chatmessage.save(disable_history=True)
|
chatmessage.save(disable_history=True)
|
||||||
@ -97,3 +109,14 @@ class ChatMessageViewSet(
|
|||||||
ReturnDict(id=chatmessage.id, serializer=serializer),
|
ReturnDict(id=chatmessage.id, serializer=serializer),
|
||||||
status=status.HTTP_201_CREATED,
|
status=status.HTTP_201_CREATED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
if (
|
||||||
|
not has_perm(self.request.user, "chat.can_manage")
|
||||||
|
and self.get_object().user_id != self.request.user.id
|
||||||
|
):
|
||||||
|
self.permission_denied(request)
|
||||||
|
|
||||||
|
disable_history()
|
||||||
|
|
||||||
|
return super().destroy(request, *args, **kwargs)
|
||||||
|
@ -110,6 +110,8 @@ if ENABLE_SAML:
|
|||||||
# Controls if electronic voting (means non-analog polls) are enabled.
|
# Controls if electronic voting (means non-analog polls) are enabled.
|
||||||
ENABLE_ELECTRONIC_VOTING = False
|
ENABLE_ELECTRONIC_VOTING = False
|
||||||
|
|
||||||
|
# Controls if chat should be enabled
|
||||||
|
ENABLE_CHAT = False
|
||||||
|
|
||||||
# Jitsi integration
|
# Jitsi integration
|
||||||
# JITSI_DOMAIN = None
|
# JITSI_DOMAIN = None
|
||||||
|
@ -26,7 +26,10 @@ def test_motion_db_queries():
|
|||||||
|
|
||||||
for i2 in range(10):
|
for i2 in range(10):
|
||||||
ChatMessage.objects.create(
|
ChatMessage.objects.create(
|
||||||
text=f"text-{i1}-{i2}", username=f"user-{i1}-{i2}", chatgroup=chatgroup
|
text=f"text-{i1}-{i2}",
|
||||||
|
username=f"user-{i1}-{i2}",
|
||||||
|
user_id=i1 * 1000 + i2,
|
||||||
|
chatgroup=chatgroup,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert count_queries(ChatGroup.get_elements)() == 2
|
assert count_queries(ChatGroup.get_elements)() == 2
|
||||||
|
Loading…
Reference in New Issue
Block a user