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 { 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 { ConstantsService } from './core/core-services/constants.service';
|
||||
import { CountUsersService } from './core/ui-services/count-users.service';
|
||||
|
@ -6,6 +6,11 @@ export class ChatMessage extends BaseModel<ChatMessage> {
|
||||
public id: number;
|
||||
public text: 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 chatgroup_id: number;
|
||||
|
||||
|
@ -5,6 +5,7 @@ import * as moment from 'moment';
|
||||
|
||||
/**
|
||||
* pipe to convert and translate dates
|
||||
* requires a "date"object
|
||||
*/
|
||||
@Pipe({
|
||||
name: 'localizedDate',
|
||||
@ -13,13 +14,13 @@ import * as moment from 'moment';
|
||||
export class LocalizedDatePipe implements PipeTransform {
|
||||
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;
|
||||
if (!value) {
|
||||
if (!date) {
|
||||
return '';
|
||||
}
|
||||
moment.locale(lang);
|
||||
const dateLocale = moment.unix(value).local();
|
||||
const dateLocale = moment.unix(date.getTime() / 1000).local();
|
||||
return dateLocale.format(dateFormat);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
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';
|
||||
|
||||
const routes: Route[] = [
|
||||
@ -9,8 +8,7 @@ const routes: Route[] = [
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
component: ChatGroupListComponent
|
||||
},
|
||||
{ path: ':id', component: ChatGroupDetailComponent }
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -3,11 +3,20 @@ import { NgModule } from '@angular/core';
|
||||
|
||||
import { ChatGroupDetailComponent } from './components/chat-group-detail/chat-group-detail.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 { 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';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, ChatRoutingModule, SharedModule],
|
||||
declarations: [ChatGroupListComponent, ChatGroupDetailComponent]
|
||||
declarations: [
|
||||
ChatGroupListComponent,
|
||||
ChatGroupDetailComponent,
|
||||
ChatTabsComponent,
|
||||
EditChatGroupDialogComponent,
|
||||
ChatMessageComponent
|
||||
]
|
||||
})
|
||||
export class ChatModule {}
|
||||
|
@ -1,53 +1,45 @@
|
||||
<os-head-bar [nav]="false">
|
||||
<!-- Title -->
|
||||
<div *ngIf="chatgroup" class="title-slot">
|
||||
<h2>{{ chatgroup.name }}</h2>
|
||||
</div>
|
||||
|
||||
<!-- Menu -->
|
||||
<div class="menu-slot" *osPerms="'chat.can_manage'">
|
||||
<button type="button" mat-icon-button [matMenuTriggerFor]="chatgroupMenu">
|
||||
<div class="chat-header" *osPerms="permission.chatCanManage">
|
||||
<os-icon-container
|
||||
icon="group"
|
||||
*ngIf="chatGroup.access_groups.length"
|
||||
matTooltip="{{ 'Access groups' | translate }}"
|
||||
>
|
||||
<span *ngFor="let group of chatGroup.access_groups | slice: 0:3; let last = last">
|
||||
<span>{{ group.getTitle() | translate }}</span>
|
||||
<span *ngIf="!last">, </span>
|
||||
<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>
|
||||
</div>
|
||||
</os-head-bar>
|
||||
|
||||
|
||||
<div class="messages">
|
||||
<div *ngFor="let message of chatMessages">
|
||||
{{ message.username }} ({{ message.timestamp }}): {{ message.text }}
|
||||
<div class="chat-list-wrapper">
|
||||
<cdk-virtual-scroll-viewport class="chat-message-list" itemSize="70">
|
||||
<div *cdkVirtualFor="let message of chatMessages" class="chat-message">
|
||||
<os-chat-message [message]="message" (deleteEvent)="deleteChatMessage(message)"></os-chat-message>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<form [formGroup]="newMessageForm">
|
||||
<mat-form-field>
|
||||
<input
|
||||
type="text"
|
||||
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>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
</div>
|
||||
|
||||
<!-- The menu content -->
|
||||
<mat-menu #chatgroupMenu="matMenu">
|
||||
<button mat-menu-item (click)="clearChat()" class="red-warning-text">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'Clear the chat' | translate }}</span>
|
||||
<ng-container *osPerms="permission.chatCanManage">
|
||||
<button mat-menu-item (click)="editChat()">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>{{ 'Edit chat' | translate }}</span>
|
||||
</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>
|
||||
|
@ -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 { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { CdkVirtualScrollViewport, ExtendedScrollToOptions } from '@angular/cdk/scrolling';
|
||||
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 { Title } from '@angular/platform-browser';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { ChatGroupRepositoryService } from 'app/core/repositories/chat/chat-group-repository.service';
|
||||
import { ChatMessageRepositoryService } from 'app/core/repositories/chat/chat-message-repository.service';
|
||||
import { ChatNotificationService } from 'app/core/ui-services/chat-notification.service';
|
||||
import { ChatMessage } from 'app/shared/models/chat/chat-message';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
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 { 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 { ViewChatMessage } from '../../models/view-chat-message';
|
||||
|
||||
@Component({
|
||||
selector: 'os-chat-group-detail',
|
||||
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 {
|
||||
public newMessageForm: FormGroup;
|
||||
public chatgroup: ViewChatGroup;
|
||||
public chatgroupId: number;
|
||||
export class ChatGroupDetailComponent extends BaseViewComponentDirective implements OnInit, AfterViewInit, OnDestroy {
|
||||
@Input()
|
||||
public chatGroup: ViewChatGroup;
|
||||
|
||||
@ViewChild(CdkVirtualScrollViewport)
|
||||
public virtualScrollViewport?: CdkVirtualScrollViewport;
|
||||
|
||||
public chatGroupId: number;
|
||||
|
||||
public chatMessages: ViewChatMessage[] = [];
|
||||
|
||||
public get isOnBottomOfChat(): boolean {
|
||||
const isOnBottom = this.virtualScrollViewport?.measureScrollOffset('bottom') === 0;
|
||||
return isOnBottom;
|
||||
}
|
||||
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
protected translate: TranslateService,
|
||||
matSnackBar: MatSnackBar,
|
||||
private chatGroupRepo: ChatGroupRepositoryService,
|
||||
private repo: ChatGroupRepositoryService,
|
||||
private chatMessageRepo: ChatMessageRepositoryService,
|
||||
private route: ActivatedRoute,
|
||||
private formBuilder: FormBuilder,
|
||||
private chatNotificationService: ChatNotificationService
|
||||
private chatNotificationService: ChatNotificationService,
|
||||
private dialog: MatDialog,
|
||||
private promptService: PromptService,
|
||||
private cd: ChangeDetectorRef
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
}
|
||||
|
||||
this.newMessageForm = this.formBuilder.group({
|
||||
text: ['', Validators.required]
|
||||
public ngOnInit(): void {
|
||||
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);
|
||||
|
||||
this.subscriptions.push(
|
||||
this.chatGroupRepo.getViewModelObservable(this.chatgroupId).subscribe(chatGroup => {
|
||||
if (chatGroup) {
|
||||
super.setTitle(`${this.translate.instant('Chat group')} - ${chatGroup.getTitle()}`);
|
||||
this.chatgroup = chatGroup;
|
||||
if (this.isOnBottomOfChat) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
}),
|
||||
this.chatMessageRepo.getViewModelListBehaviorSubject().subscribe(chatMessages => {
|
||||
this.chatMessages = chatMessages.filter(message => message.chatgroup_id === this.chatgroup.id);
|
||||
this.cd.markForCheck();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
super.setTitle('Chat group');
|
||||
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 ngAfterViewInit(): void {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
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 -->
|
||||
<div class="title-slot">
|
||||
<h2>{{ 'Chat groups' | translate }}</h2>
|
||||
<h2>{{ 'Chat' | translate }}</h2>
|
||||
</div>
|
||||
</os-head-bar>
|
||||
|
||||
<div *ngFor="let chatGroup of chatGroups">
|
||||
<a [routerLink]="'./' + chatGroup.id">{{ chatGroup.name }}</a><br/>
|
||||
{{ chatGroup.access_groups.length ? chatGroup.access_groups : 'No access groups, public to all' }}<br/>
|
||||
<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>
|
||||
<mat-card class="os-card spacer-bottom-340">
|
||||
<os-chat-tabs></os-chat-tabs>
|
||||
</mat-card>
|
||||
|
@ -1,19 +1,20 @@
|
||||
import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
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 { 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 { infoDialogSettings } from 'app/shared/utils/dialog-settings';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
@ -22,99 +23,39 @@ import { ViewChatGroup } from '../../models/view-chat-group';
|
||||
styleUrls: ['./chat-group-list.component.scss']
|
||||
})
|
||||
export class ChatGroupListComponent extends BaseViewComponentDirective implements OnInit {
|
||||
@ViewChild('createUpdateDialog', { static: true })
|
||||
private createUpdateDialog: TemplateRef<string>;
|
||||
|
||||
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 canManage(): boolean {
|
||||
return this.operator.hasPerms(this.permission.chatCanManage);
|
||||
}
|
||||
|
||||
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(
|
||||
titleService: Title,
|
||||
protected translate: TranslateService,
|
||||
matSnackBar: MatSnackBar,
|
||||
private repo: ChatGroupRepositoryService,
|
||||
private formBuilder: FormBuilder,
|
||||
private groupRepo: GroupRepositoryService,
|
||||
private dialog: MatDialog,
|
||||
private operator: OperatorService,
|
||||
private chatNotificationService: ChatNotificationService
|
||||
private operator: OperatorService
|
||||
) {
|
||||
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 {
|
||||
super.setTitle('Chat groups');
|
||||
super.setTitle('Chat');
|
||||
}
|
||||
|
||||
public createNewChatGroup(): void {
|
||||
if (this.isEdit) {
|
||||
return;
|
||||
}
|
||||
const dialogRef = this.dialog.open(EditChatGroupDialogComponent, {
|
||||
data: null,
|
||||
...infoDialogSettings
|
||||
});
|
||||
|
||||
this.isEdit = true;
|
||||
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 => {
|
||||
dialogRef.afterClosed().subscribe((res: ChatGroupData) => {
|
||||
if (res) {
|
||||
this.save();
|
||||
this.save(res);
|
||||
}
|
||||
this.isEdit = false;
|
||||
});
|
||||
}
|
||||
|
||||
public save(): void {
|
||||
if (this.isCreateMode) {
|
||||
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];
|
||||
public save(createData: ChatGroupData): void {
|
||||
this.repo.create(createData as ChatGroup).catch(this.raiseError);
|
||||
}
|
||||
}
|
||||
|
@ -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 { ViewChatMessage } from 'app/site/chat/models/view-chat-message';
|
||||
import { ChatMessageRepositoryService } from '../repositories/chat/chat-message-repository.service';
|
||||
import { StorageService } from '../core-services/storage.service';
|
||||
import { ChatMessageRepositoryService } from '../../../core/repositories/chat/chat-message-repository.service';
|
||||
import { StorageService } from '../../../core/core-services/storage.service';
|
||||
|
||||
interface LastMessageTimestampsSeen {
|
||||
[chatgroupId: number]: Date;
|
@ -2,9 +2,9 @@ import { Injectable } from '@angular/core';
|
||||
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
import { ChatGroupRepositoryService } from '../repositories/chat/chat-group-repository.service';
|
||||
import { ConstantsService } from '../core-services/constants.service';
|
||||
import { OperatorService, Permission } from '../core-services/operator.service';
|
||||
import { ChatGroupRepositoryService } from '../../../core/repositories/chat/chat-group-repository.service';
|
||||
import { ConstantsService } from '../../../core/core-services/constants.service';
|
||||
import { OperatorService, Permission } from '../../../core/core-services/operator.service';
|
||||
|
||||
interface OpenSlidesSettings {
|
||||
ENABLE_CHAT: boolean;
|
@ -48,10 +48,16 @@
|
||||
routerLink="/chat"
|
||||
routerLinkActive="active"
|
||||
>
|
||||
<mat-icon *ngIf="chatNotificationAmount <= 0">chat</mat-icon>
|
||||
<mat-icon *ngIf="chatNotificationAmount > 0" class="unread-chat">notification_important</mat-icon>
|
||||
<span>{{ 'Chat' | translate }}</span>
|
||||
<span *ngIf="chatNotificationAmount > 0">({{ chatNotificationAmount }})</span>
|
||||
<mat-icon
|
||||
[matBadgeHidden]="chatNotificationAmount === 0"
|
||||
[matBadge]="chatNotificationAmount"
|
||||
matBadgeColor="accent"
|
||||
matBadgeOverlap="false"
|
||||
>chat</mat-icon
|
||||
>
|
||||
<span>
|
||||
{{ 'Chat' | translate }}
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
[@navItemAnim]
|
||||
|
@ -11,10 +11,10 @@ import { filter } from 'rxjs/operators';
|
||||
|
||||
import { navItemAnim } from '../shared/animations';
|
||||
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 { 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 { MainMenuEntry, MainMenuService } from '../core/core-services/main-menu.service';
|
||||
import { OpenSlidesStatusService } from '../core/core-services/openslides-status.service';
|
||||
|
@ -173,10 +173,25 @@
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
color: mat-color($primary);
|
||||
}
|
||||
|
@ -462,6 +462,10 @@ button.mat-menu-item.selected {
|
||||
margin-bottom: 60px !important;
|
||||
}
|
||||
|
||||
.spacer-bottom-340 {
|
||||
margin-bottom: 340px !important;
|
||||
}
|
||||
|
||||
.spacer-left-10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
@ -62,6 +62,7 @@ POSTFIX_RELAYHOST=
|
||||
# Features
|
||||
ENABLE_SAML=
|
||||
ENABLE_ELECTRONIC_VOTING=
|
||||
ENABLE_CHAT=
|
||||
DEMO_USERS=
|
||||
|
||||
# Connections
|
||||
|
@ -70,6 +70,7 @@ x-osserver-env: &default-osserver-env
|
||||
EMAIL_USE_TLS: "ifenvelse(`EMAIL_USE_TLS',)"
|
||||
EMAIL_TIMEOUT: "ifenvelse(`EMAIL_TIMEOUT',)"
|
||||
ENABLE_ELECTRONIC_VOTING: "ifenvelse(`ENABLE_ELECTRONIC_VOTING', False)"
|
||||
ENABLE_CHAT: "ifenvelse(`ENABLE_CHAT', False)"
|
||||
ENABLE_SAML: "ifenvelse(`ENABLE_SAML', False)"
|
||||
INSTANCE_DOMAIN: "ifenvelse(`INSTANCE_DOMAIN', http://example.com:8000)"
|
||||
JITSI_DOMAIN: "ifenvelse(`JITSI_DOMAIN',)"
|
||||
|
@ -69,6 +69,7 @@ x-osserver-env: &default-osserver-env
|
||||
EMAIL_USE_TLS: "ifenvelse(`EMAIL_USE_TLS',)"
|
||||
EMAIL_TIMEOUT: "ifenvelse(`EMAIL_TIMEOUT',)"
|
||||
ENABLE_ELECTRONIC_VOTING: "ifenvelse(`ENABLE_ELECTRONIC_VOTING', False)"
|
||||
ENABLE_CHAT: "ifenvelse(`ENABLE_CHAT', False)"
|
||||
ENABLE_SAML: "ifenvelse(`ENABLE_SAML', False)"
|
||||
INSTANCE_DOMAIN: "ifenvelse(`INSTANCE_DOMAIN', http://example.com:8000)"
|
||||
JITSI_DOMAIN: "ifenvelse(`JITSI_DOMAIN',)"
|
||||
|
@ -65,6 +65,13 @@ To enable it, set::
|
||||
|
||||
ENABLE_ELECTRONIC_VOTING = True
|
||||
|
||||
Chat
|
||||
=================
|
||||
|
||||
Is disabled by default. Enable the chatting feature::
|
||||
|
||||
ENABLE_CHAT = True
|
||||
|
||||
|
||||
Jitsi integration
|
||||
=================
|
||||
|
@ -125,6 +125,9 @@ if ENABLE_SAML:
|
||||
# Controls if electronic voting (means non-analog polls) are enabled.
|
||||
ENABLE_ELECTRONIC_VOTING = get_env("ENABLE_ELECTRONIC_VOTING", False, bool)
|
||||
|
||||
# Enable Chat
|
||||
ENABLE_CHAT = get_env("ENABLE_CHAT", False, bool)
|
||||
|
||||
# Jitsi integration
|
||||
JITSI_DOMAIN = get_env("JITSI_DOMAIN", None)
|
||||
JITSI_ROOM_NAME = get_env("JITSI_ROOM_NAME", None)
|
||||
|
@ -55,6 +55,7 @@ class Migration(migrations.Migration):
|
||||
("text", models.CharField(max_length=512)),
|
||||
("timestamp", models.DateTimeField(auto_now_add=True)),
|
||||
("username", models.CharField(max_length=256)),
|
||||
("user_id", models.IntegerField()),
|
||||
(
|
||||
"chatgroup",
|
||||
models.ForeignKey(
|
||||
|
@ -78,6 +78,7 @@ class ChatMessage(RESTModelMixin, models.Model):
|
||||
ChatGroup, on_delete=CASCADE_AND_AUTOUPDATE, related_name="messages"
|
||||
)
|
||||
username = models.CharField(max_length=256)
|
||||
user_id = models.IntegerField()
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
|
@ -45,9 +45,13 @@ class ChatMessageSerializer(ModelSerializer):
|
||||
"chatgroup",
|
||||
"timestamp",
|
||||
"username",
|
||||
"user_id",
|
||||
"access_groups_id",
|
||||
)
|
||||
read_only_fields = ("username",)
|
||||
read_only_fields = (
|
||||
"username",
|
||||
"user_id",
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
if "text" in data:
|
||||
|
@ -1,10 +1,16 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from rest_framework.utils.serializer_helpers import ReturnDict
|
||||
|
||||
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 (
|
||||
CreateModelMixin,
|
||||
DestroyModelMixin,
|
||||
GenericViewSet,
|
||||
ListModelMixin,
|
||||
ModelViewSet,
|
||||
@ -64,7 +70,11 @@ class ChatGroupViewSet(ModelViewSet):
|
||||
|
||||
|
||||
class ChatMessageViewSet(
|
||||
ListModelMixin, RetrieveModelMixin, CreateModelMixin, GenericViewSet
|
||||
ListModelMixin,
|
||||
RetrieveModelMixin,
|
||||
CreateModelMixin,
|
||||
DestroyModelMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
"""
|
||||
API endpoint for chat groups.
|
||||
@ -76,7 +86,8 @@ class ChatMessageViewSet(
|
||||
queryset = ChatMessage.objects.all()
|
||||
|
||||
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):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
@ -89,6 +100,7 @@ class ChatMessageViewSet(
|
||||
validated_data = {
|
||||
**serializer.validated_data,
|
||||
"username": self.request.user.short_name(),
|
||||
"user_id": self.request.user.id,
|
||||
}
|
||||
chatmessage = ChatMessage(**validated_data)
|
||||
chatmessage.save(disable_history=True)
|
||||
@ -97,3 +109,14 @@ class ChatMessageViewSet(
|
||||
ReturnDict(id=chatmessage.id, serializer=serializer),
|
||||
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.
|
||||
ENABLE_ELECTRONIC_VOTING = False
|
||||
|
||||
# Controls if chat should be enabled
|
||||
ENABLE_CHAT = False
|
||||
|
||||
# Jitsi integration
|
||||
# JITSI_DOMAIN = None
|
||||
|
@ -26,7 +26,10 @@ def test_motion_db_queries():
|
||||
|
||||
for i2 in range(10):
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user