diff --git a/autoupdate b/autoupdate index 3380911a7..756043511 160000 --- a/autoupdate +++ b/autoupdate @@ -1 +1 @@ -Subproject commit 3380911a7e9cb4a906d3729d30a164ed3d59fd22 +Subproject commit 756043511cc00b9fd4b42cc3a7ba0d8c16897895 diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index b12617e2b..698657ad2 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -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'; diff --git a/client/src/app/shared/models/chat/chat-message.ts b/client/src/app/shared/models/chat/chat-message.ts index 320d3b5cc..5520d9d7d 100644 --- a/client/src/app/shared/models/chat/chat-message.ts +++ b/client/src/app/shared/models/chat/chat-message.ts @@ -6,6 +6,11 @@ export class ChatMessage extends BaseModel { 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; diff --git a/client/src/app/shared/pipes/localized-date.pipe.ts b/client/src/app/shared/pipes/localized-date.pipe.ts index 61606961e..7e891f25c 100644 --- a/client/src/app/shared/pipes/localized-date.pipe.ts +++ b/client/src/app/shared/pipes/localized-date.pipe.ts @@ -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); } } diff --git a/client/src/app/site/chat/chat-routing.module.ts b/client/src/app/site/chat/chat-routing.module.ts index 013456dfc..67b6dceac 100644 --- a/client/src/app/site/chat/chat-routing.module.ts +++ b/client/src/app/site/chat/chat-routing.module.ts @@ -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({ diff --git a/client/src/app/site/chat/chat.module.ts b/client/src/app/site/chat/chat.module.ts index d9a5c6327..e76142fd9 100644 --- a/client/src/app/site/chat/chat.module.ts +++ b/client/src/app/site/chat/chat.module.ts @@ -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 {} diff --git a/client/src/app/site/chat/components/chat-group-detail/chat-group-detail.component.html b/client/src/app/site/chat/components/chat-group-detail/chat-group-detail.component.html index be51c5c5f..c93bd4e1b 100644 --- a/client/src/app/site/chat/components/chat-group-detail/chat-group-detail.component.html +++ b/client/src/app/site/chat/components/chat-group-detail/chat-group-detail.component.html @@ -1,53 +1,45 @@ - - -
-

{{ chatgroup.name }}

-
- - - -
- - -
-
- {{ message.username }} ({{ message.timestamp }}): {{ message.text }} -
+
+ + + {{ group.getTitle() | translate }} + , + ... + + +
- -
-
- - - {{ 'Required' | translate }} - -
- +
+ +
+ +
+
- + + + + + + + + diff --git a/client/src/app/site/chat/components/chat-group-detail/chat-group-detail.component.scss b/client/src/app/site/chat/components/chat-group-detail/chat-group-detail.component.scss index e69de29bb..b13ddb7e6 100644 --- a/client/src/app/site/chat/components/chat-group-detail/chat-group-detail.component.scss +++ b/client/src/app/site/chat/components/chat-group-detail/chat-group-detail.component.scss @@ -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; +} diff --git a/client/src/app/site/chat/components/chat-group-detail/chat-group-detail.component.ts b/client/src/app/site/chat/components/chat-group-detail/chat-group-detail.component.ts index f7ab1ffa7..6e7dde54b 100644 --- a/client/src/app/site/chat/components/chat-group-detail/chat-group-detail.component.ts +++ b/client/src/app/site/chat/components/chat-group-detail/chat-group-detail.component.ts @@ -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] - }); - - this.chatgroupId = parseInt(this.route.snapshot.params.id, 10); - + public ngOnInit(): void { + this.chatGroupId = this.chatGroup.id; + this.chatNotificationService.openChat(this.chatGroupId); 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.chatMessageRepo.getViewModelListBehaviorSubject().subscribe(chatMessages => { - this.chatMessages = chatMessages.filter(message => message.chatgroup_id === this.chatgroup.id); + this.chatMessages = chatMessages.filter(message => { + return message.chatgroup_id === this.chatGroup.id; + }); + + if (this.isOnBottomOfChat) { + this.scrollToBottom(); + } + 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 { + await this.repo.update(chatData as ChatGroup, this.chatGroup).catch(this.raiseError); + this.cd.markForCheck(); + } + + public async clearChat(): Promise { + 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 { + 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 { + 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(); + } } } diff --git a/client/src/app/site/chat/components/chat-group-list/chat-group-list.component.html b/client/src/app/site/chat/components/chat-group-list/chat-group-list.component.html index dbac93a3e..c9075b0f2 100644 --- a/client/src/app/site/chat/components/chat-group-list/chat-group-list.component.html +++ b/client/src/app/site/chat/components/chat-group-list/chat-group-list.component.html @@ -1,60 +1,10 @@ - +
-

{{ 'Chat groups' | translate }}

+

{{ 'Chat' | translate }}

-
- {{ chatGroup.name }}
- {{ chatGroup.access_groups.length ? chatGroup.access_groups : 'No access groups, public to all' }}
- -
{{ amountNotification(chatGroup) }} NEW MESSAGES
-
-
- - - -

{{ 'Create new chat group' | translate }}

-

{{ 'Edit details for' | translate }} {{ editModel.name }}

-
-
- - - {{ 'Required' | translate }} - - - - - -
-
-
- - -
-
+ + + diff --git a/client/src/app/site/chat/components/chat-group-list/chat-group-list.component.ts b/client/src/app/site/chat/components/chat-group-list/chat-group-list.component.ts index e8d18cf23..0d8194f94 100644 --- a/client/src/app/site/chat/components/chat-group-list/chat-group-list.component.ts +++ b/client/src/app/site/chat/components/chat-group-list/chat-group-list.component.ts @@ -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; - - public createUpdateForm: FormGroup; - public groupsBehaviorSubject: BehaviorSubject; - - 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); } } diff --git a/client/src/app/site/chat/components/chat-message/chat-message.component.html b/client/src/app/site/chat/components/chat-message/chat-message.component.html new file mode 100644 index 000000000..cf0bcdca6 --- /dev/null +++ b/client/src/app/site/chat/components/chat-message/chat-message.component.html @@ -0,0 +1,21 @@ +
+
+ {{ author }} +
+
+ {{ text }} +
{{ date | localizedDate }}
+
+
+ + + + + diff --git a/client/src/app/site/chat/components/chat-message/chat-message.component.scss b/client/src/app/site/chat/components/chat-message/chat-message.component.scss new file mode 100644 index 000000000..0a33d265a --- /dev/null +++ b/client/src/app/site/chat/components/chat-message/chat-message.component.scss @@ -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; +} diff --git a/client/src/app/site/chat/components/chat-message/chat-message.component.spec.ts b/client/src/app/site/chat/components/chat-message/chat-message.component.spec.ts new file mode 100644 index 000000000..110301cf3 --- /dev/null +++ b/client/src/app/site/chat/components/chat-message/chat-message.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/client/src/app/site/chat/components/chat-message/chat-message.component.ts b/client/src/app/site/chat/components/chat-message/chat-message.component.ts new file mode 100644 index 000000000..454d9b7af --- /dev/null +++ b/client/src/app/site/chat/components/chat-message/chat-message.component.ts @@ -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(); + } +} diff --git a/client/src/app/site/chat/components/chat-tabs/chat-tabs.component.html b/client/src/app/site/chat/components/chat-tabs/chat-tabs.component.html new file mode 100644 index 000000000..44db6b585 --- /dev/null +++ b/client/src/app/site/chat/components/chat-tabs/chat-tabs.component.html @@ -0,0 +1,41 @@ + + + + + {{ chat.name }} + + + + + + + + +
+ + {{ 'No chat groups available' | translate }} + +
+ + +
+ + + {{ 'Message' | translate }} + + +
diff --git a/client/src/app/site/chat/components/chat-tabs/chat-tabs.component.scss b/client/src/app/site/chat/components/chat-tabs/chat-tabs.component.scss new file mode 100644 index 000000000..c4e2b592c --- /dev/null +++ b/client/src/app/site/chat/components/chat-tabs/chat-tabs.component.scss @@ -0,0 +1,7 @@ +.chat-form-field { + width: 100%; +} + +.chat-input { + // todo +} diff --git a/client/src/app/site/chat/components/chat-tabs/chat-tabs.component.spec.ts b/client/src/app/site/chat/components/chat-tabs/chat-tabs.component.spec.ts new file mode 100644 index 000000000..e4489d6b8 --- /dev/null +++ b/client/src/app/site/chat/components/chat-tabs/chat-tabs.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ChatTabsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/chat/components/chat-tabs/chat-tabs.component.ts b/client/src/app/site/chat/components/chat-tabs/chat-tabs.component.ts new file mode 100644 index 000000000..e634144c5 --- /dev/null +++ b/client/src/app/site/chat/components/chat-tabs/chat-tabs.component.ts @@ -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; + 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(); + } +} diff --git a/client/src/app/site/chat/components/edit-chat-group-dialog/edit-chat-group-dialog.component.html b/client/src/app/site/chat/components/edit-chat-group-dialog/edit-chat-group-dialog.component.html new file mode 100644 index 000000000..9320b025e --- /dev/null +++ b/client/src/app/site/chat/components/edit-chat-group-dialog/edit-chat-group-dialog.component.html @@ -0,0 +1,53 @@ +

+ + {{ 'Create new chat group' | translate }} + + + {{ 'Edit details for' | translate }} {{ previousChatGroupName }} +

+ +
+
+ + + + {{ 'Required' | translate }} + + + + + + +
+ +
+ + + + + +
+
diff --git a/client/src/app/site/chat/components/edit-chat-group-dialog/edit-chat-group-dialog.component.scss b/client/src/app/site/chat/components/edit-chat-group-dialog/edit-chat-group-dialog.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/app/site/chat/components/edit-chat-group-dialog/edit-chat-group-dialog.component.spec.ts b/client/src/app/site/chat/components/edit-chat-group-dialog/edit-chat-group-dialog.component.spec.ts new file mode 100644 index 000000000..8ce5c9c70 --- /dev/null +++ b/client/src/app/site/chat/components/edit-chat-group-dialog/edit-chat-group-dialog.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/client/src/app/site/chat/components/edit-chat-group-dialog/edit-chat-group-dialog.component.ts b/client/src/app/site/chat/components/edit-chat-group-dialog/edit-chat-group-dialog.component.ts new file mode 100644 index 000000000..8f505d037 --- /dev/null +++ b/client/src/app/site/chat/components/edit-chat-group-dialog/edit-chat-group-dialog.component.ts @@ -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; + + 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(); + } +} diff --git a/client/src/app/core/ui-services/chat-notification.service.ts b/client/src/app/site/chat/services/chat-notification.service.ts similarity index 95% rename from client/src/app/core/ui-services/chat-notification.service.ts rename to client/src/app/site/chat/services/chat-notification.service.ts index 0754a18a5..7c59dc57c 100644 --- a/client/src/app/core/ui-services/chat-notification.service.ts +++ b/client/src/app/site/chat/services/chat-notification.service.ts @@ -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; diff --git a/client/src/app/core/ui-services/chat.service.ts b/client/src/app/site/chat/services/chat.service.ts similarity index 82% rename from client/src/app/core/ui-services/chat.service.ts rename to client/src/app/site/chat/services/chat.service.ts index a1716e921..e6c31f444 100644 --- a/client/src/app/core/ui-services/chat.service.ts +++ b/client/src/app/site/chat/services/chat.service.ts @@ -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; diff --git a/client/src/app/site/site.component.html b/client/src/app/site/site.component.html index a5981994e..d3d03bc27 100644 --- a/client/src/app/site/site.component.html +++ b/client/src/app/site/site.component.html @@ -48,10 +48,16 @@ routerLink="/chat" routerLinkActive="active" > - chat - notification_important - {{ 'Chat' | translate }} - ({{ chatNotificationAmount }}) + chat + + {{ 'Chat' | translate }} +