diff --git a/src/app/shared/message-board/message-board.component.html b/src/app/shared/message-board/message-board.component.html new file mode 100644 index 0000000000..80a37d8d48 --- /dev/null +++ b/src/app/shared/message-board/message-board.component.html @@ -0,0 +1,67 @@ + + + New + + + + + + + + + diff --git a/src/app/shared/message-board/message-board.component.scss b/src/app/shared/message-board/message-board.component.scss new file mode 100644 index 0000000000..b38edffbad --- /dev/null +++ b/src/app/shared/message-board/message-board.component.scss @@ -0,0 +1,56 @@ +@import '../../../styles/variables'; + +.modal-header { + background-color: #2B4E72; + color: white; +} + +.modal-footer { + width: 100%; + display: block; +} + +.modal-footer :not(:first-child){ + margin: 0 auto !important; +} + +.close { + position: relative; + top: 0; + right:0; +} + +textarea { + //resize: none; + margin-bottom: 15px; +} + +.chat +{ + list-style: none; + margin: 10px; + padding: 0; +} + +::-webkit-scrollbar-track +{ + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); + background-color: #FAF5F5; +} + +::-webkit-scrollbar +{ + width: 12px; + background-color: #F5F5FC; +} + +::-webkit-scrollbar-thumb +{ + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); + background-color: #555; +} + +.notification { + left: -20px; + top: -25px +} diff --git a/src/app/shared/message-board/message-board.component.ts b/src/app/shared/message-board/message-board.component.ts new file mode 100644 index 0000000000..3b5b2186eb --- /dev/null +++ b/src/app/shared/message-board/message-board.component.ts @@ -0,0 +1,245 @@ +import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +import { combineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; +import { + distinctUntilChanged, + filter, + find, + first, + flatMap, + map, + mergeMap, + reduce, + startWith, + tap, + withLatestFrom +} from 'rxjs/operators'; +import { select, Store } from '@ngrx/store'; +import { NgbActiveModal, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateService } from '@ngx-translate/core'; + +import { Bitstream } from '../../core/shared/bitstream.model'; +import { MessageService } from '../../core/message/message.service'; +import { NotificationsService } from '../notifications/notifications.service'; +import { hasValue, isNotEmpty } from '../empty.util'; +import { Item } from '../../core/shared/item.model'; +import { RemoteData } from '../../core/data/remote-data'; +import { MessageDataResponse } from '../../core/message/message-data-response'; +import { AppState } from '../../app.reducer'; +import { getAuthenticatedUser } from '../../core/auth/selectors'; +import { EPerson } from '../../core/eperson/models/eperson.model'; + +@Component({ + selector: 'ds-message-board', + styleUrls: ['./message-board.component.scss'], + templateUrl: './message-board.component.html', + providers: [ + NgbActiveModal, + ] +}) + +export class MessageBoardComponent implements OnDestroy { + @Input() dso: any; + @Input() tooltipMessage: string; + @Output() refresh = new EventEmitter(); + + public item$: Observable; + public submitter$: Observable; + public user$: Observable; + public unreadMessages$: Observable = observableOf([]); + public modalRef: NgbModalRef; + public itemUUID$: Observable; + public messages$: Observable = observableOf([]); + public isSubmitter$: Observable; + public messageForm: FormGroup; + public processingMessage = false; + + private subs: Subscription[] = []; + private rememberEmitUnread = false; + private rememberEmitRead = false; + + constructor(private formBuilder: FormBuilder, + public msgService: MessageService, + private modalService: NgbModal, + private notificationsService: NotificationsService, + private store: Store, + private translate: TranslateService) { + } + + ngOnInit() { + // set formGroup + this.messageForm = this.formBuilder.group({ + textSubject: ['', Validators.required], + textDescription: ['', Validators.required] + }); + + this.user$ = this.store.pipe( + select(getAuthenticatedUser), + find((user: EPerson) => isNotEmpty(user)), + map((user: EPerson) => user), + tap((u) => console.log(u))); + + this.item$ = this.dso.item.pipe( + find((rd: RemoteData) => (rd.hasSucceeded && isNotEmpty(rd.payload))), + map((rd: RemoteData) => rd.payload), + tap((u) => console.log(u))); + + this.submitter$ = (this.dso.submitter as Observable>).pipe( + find((rd: RemoteData) => rd.hasSucceeded && isNotEmpty(rd.payload)), + map((rd: RemoteData) => rd.payload), + tap((u) => console.log(u))); + + this.isSubmitter$ = combineLatest(this.user$, this.submitter$).pipe( + filter(([user, submitter]) => isNotEmpty(user) && isNotEmpty(submitter)), + map(([user, submitter]) => user.uuid === submitter.uuid), + tap((u) => console.log(u))); + + this.messages$ = this.item$.pipe( + find((item: Item) => isNotEmpty(item)), + flatMap((item: Item) => item.getBitstreamsByBundleName('MESSAGE')), + filter((bitStreams: Bitstream[]) => isNotEmpty(bitStreams)), + startWith([]), + distinctUntilChanged(), + tap((u) => console.log(u))); + + this.unreadMessages$ = this.messages$.pipe( + filter((messages: Bitstream[]) => isNotEmpty(messages)), + flatMap((bitStream: Bitstream) => + observableOf(bitStream).pipe( + withLatestFrom(this.isUnread(bitStream)) + ) + ), + filter(([bitStream, isUnread]) => isUnread), + map(([bitStream, isUnread]) => bitStream), + reduce((acc: any, value: any) => [...acc, ...value], []), + startWith([]) + ); + + this.itemUUID$ = this.item$.pipe( + find((item: Item) => isNotEmpty(item)), + map((item: Item) => item.uuid), + tap((u) => console.log(u))); + + } + + sendMessage(itemUUID) { + this.processingMessage = true; + const subject: string = this.messageForm.get('textSubject').value; + const description: string = this.messageForm.get('textDescription').value; + const body = { + uuid: itemUUID, + subject, + description + }; + this.subs.push( + this.msgService.createMessage(body).pipe( + first() + ).subscribe((res: MessageDataResponse) => { + this.processingMessage = false; + this.modalRef.dismiss('Send Message'); + if (res.hasSucceeded) { + // Refresh event + this.refresh.emit('read'); + this.notificationsService.success(null, + this.translate.get('submission.workflow.tasks.generic.success')); + } else { + this.notificationsService.error(null, + this.translate.get('submission.workflow.tasks.generic.error')); + } + }) + ); + } + + markAsUnread(msgUUID: string) { + if (msgUUID) { + const body = { + uuid: msgUUID + }; + this.subs.push( + this.msgService.markAsUnread(body).pipe( + find((res) => res.hasSucceeded) + ).subscribe((res) => { + if (!res.error) { + this.rememberEmitUnread = true; + this.rememberEmitRead = false; + } else { + this.notificationsService.error(null, this.translate.get('submission.workflow.tasks.generic.error')); + } + }) + ); + } + } + + emitRefresh() { + if (this.rememberEmitUnread && !this.rememberEmitRead) { + // Refresh event for Unread + this.refresh.emit('unread'); + } else if (!this.rememberEmitUnread && this.rememberEmitRead) { + // Refresh event for Read + this.refresh.emit('read'); + } + } + + markAsRead(msgUUID?: string) { + let ids$: Observable; + if (msgUUID) { + ids$ = observableOf([msgUUID]); + } else { + ids$ = this.unreadMessages$.pipe( + filter((messages: Bitstream[]) => isNotEmpty(messages)), + flatMap((message: Bitstream) => message.uuid), + reduce((acc: any, value: any) => [...acc, ...value], []), + startWith([]) + ) + } + + this.subs.push( + ids$.pipe( + filter((uuids) => isNotEmpty(uuids)), + mergeMap((uuid: any) => { + const body = { uuid }; + return this.msgService.markAsRead(body) + }) + ).subscribe((res: MessageDataResponse) => { + if (res.hasSucceeded) { + this.rememberEmitRead = true; + this.rememberEmitUnread = false; + } else { + this.notificationsService.error(null, this.translate.get('submission.workflow.tasks.generic.error')); + } + }) + ); + + } + + isUnread(m: Bitstream): Observable { + const accessioned = m.firstMetadataValue('dc.date.accessioned'); + const type = m.firstMetadataValue('dc.type'); + return this.isSubmitter$.pipe( + filter((isSubmitter) => isNotEmpty(isSubmitter)), + map((isSubmitter) => (!accessioned && + ((isSubmitter && type === 'outbound') || (!isSubmitter && type === 'inbound'))) + ), + startWith(false)); + } + + openMessageBoard(content) { + this.rememberEmitUnread = false; + this.rememberEmitRead = false; + this.markAsRead(); + this.modalRef = this.modalService.open(content, { size: 'lg' }); + this.modalRef.result.then((result) => { + this.emitRefresh(); + }, (reason) => { + this.emitRefresh(); + }); + } + + ngOnDestroy() { + this.subs + .filter((sub) => hasValue(sub)) + .forEach((sub) => sub.unsubscribe()); + } + +} diff --git a/src/app/shared/message-board/message/message.component.html b/src/app/shared/message-board/message/message.component.html new file mode 100644 index 0000000000..d8637e87fe --- /dev/null +++ b/src/app/shared/message-board/message/message.component.html @@ -0,0 +1,56 @@ +
  • + +
    +
    + + + {{m.findMetadata('dc.date.issued') | date: 'dd/MM/yyyy HH:mm'}} + + + + {{'mydspace.messages.mark-as-unread' | translate}} + + + + {{'mydspace.messages.mark-as-read' | translate}} + + +
    + + + + + +
    + +
    + + + + + +
    + +
    + + + +
    + {{messageContent | async}} + +
    +
    + +
  • diff --git a/src/app/shared/message-board/message/message.component.scss b/src/app/shared/message-board/message/message.component.scss new file mode 100644 index 0000000000..f7812e795f --- /dev/null +++ b/src/app/shared/message-board/message/message.component.scss @@ -0,0 +1,54 @@ +$user_bg: #e6f2ff; +$other_bg: #EFEFEF; + +li { + margin-bottom: 10px; + padding-bottom: 5px; + width: 80%; +} + +.chat-body { + padding: 5px; + border-radius: 5px; +} + +li.float-left .chat-body { + background: $other_bg; +} + +li.float-right .chat-body { + background: $user_bg; +} + +li .chat-body p { + margin: 0; + color: #777777; +} + +.description { + color: #666666; + font-style: italic; + padding: 0 15px; +} + +.pointer { + cursor: pointer; +} + +.max250 { + max-width: 250px; +} + +/deep/ .float-right div[class^="clamp-"] +.content:after { + background: linear-gradient(to right, rgba(255, 255, 255, 0), $user_bg 70%) !important; +} + +/deep/ .float-left div[class^="clamp-"] +.content:after { + background: linear-gradient(to right, rgba(255, 255, 255, 0), $other_bg 70%) !important; +} + +.truncatable { + clear: both; +} diff --git a/src/app/shared/message-board/message/message.component.ts b/src/app/shared/message-board/message/message.component.ts new file mode 100644 index 0000000000..0119b662ab --- /dev/null +++ b/src/app/shared/message-board/message/message.component.ts @@ -0,0 +1,83 @@ +import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; + +import { Observable, of as observableOf } from 'rxjs'; +import { first, flatMap } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; + +import { Bitstream } from '../../../core/shared/bitstream.model'; +import { MessageService } from '../../../core/message/message.service'; +import { isNull } from '../../empty.util'; + +@Component({ + selector: 'ds-message', + styleUrls: ['./message.component.scss'], + templateUrl: './message.component.html' +}) + +export class MessageComponent implements OnInit { + @Input() m: Bitstream; + @Input() isLast: boolean; + @Input() isSubmitter: boolean; + + @Output() emitUnread = new EventEmitter(); + @Output() emitRead = new EventEmitter(); + + public showUnread: boolean; + public showRead: boolean; + public showMessage = false; + + private _messageContent: Observable = null; + private loadingDescription = false; + + constructor(private cdr: ChangeDetectorRef, + private msgService: MessageService, + private translate: TranslateService) { + } + + ngOnInit() { + const type = this.m.firstMetadataValue('dc.type'); + + if (this.isLast) { + if ((this.isSubmitter && type === 'outbound') + || (!this.isSubmitter && type === 'inbound')) { + this.showUnread = true; + this.showRead = false; + } + } else { + this.showUnread = false; + this.showRead = false; + } + } + + toggleDescription() { + this.showMessage = !this.showMessage; + this.cdr.detectChanges(); + } + + get messageContent(): Observable { + if (isNull(this._messageContent) && !this.loadingDescription) { + this.loadingDescription = true; + this._messageContent = this.msgService.getMessageContent(this.m.content).pipe( + first(), + flatMap((res) => { + this._messageContent = res.payload ? observableOf(res.payload) : this.translate.get('mydspace.messages.no-content'); + this.loadingDescription = false; + return this._messageContent; + })); + } + return this._messageContent; + } + + markAsRead() { + this.emitRead.emit(true); + this.showUnread = true; + this.showRead = false; + } + + markAsUnread() { + this.emitUnread.emit(true); + this.showUnread = false; + this.showRead = true; + } + +}