mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-10 19:43:04 +00:00
Added message board components
This commit is contained in:
67
src/app/shared/message-board/message-board.component.html
Normal file
67
src/app/shared/message-board/message-board.component.html
Normal file
@@ -0,0 +1,67 @@
|
||||
<button
|
||||
class="btn btn-primary mt-1 mb-3"
|
||||
ngbTooltip="{{tooltipMessage | translate}}"
|
||||
(click)="openMessageBoard(content)">
|
||||
<i class="fa fa-envelope"></i>
|
||||
<span *ngIf="(messages$ | async)?.length > 0"
|
||||
class="badge badge-pill badge-secondary">{{(messages$ | async)?.length}}</span>
|
||||
</button>
|
||||
<sup class="notification">
|
||||
<span *ngIf="(unreadMessages$ | async)?.length > 0" class="badge badge-pill badge-danger font-italic">New</span>
|
||||
</sup>
|
||||
|
||||
<ng-template #content let-c="close" let-d="dismiss">
|
||||
<div class="modal-header rounded-top">
|
||||
<i class="fa fa-envelope"></i> {{'mydspace.messages.title' | translate}}
|
||||
<div class="btn-group pull-right">
|
||||
<span (click)="modalRef.dismiss('Cross click')">
|
||||
<i class="fa fa-2x fa-times" style="color: white"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<article id="content" class="rounded-bottom">
|
||||
<div id="list">
|
||||
<ul class="chat">
|
||||
<ng-container *ngFor="let m of (messages$ | async); let i = index">
|
||||
<ds-message
|
||||
[m]="m"
|
||||
[isSubmitter]="isSubmitter$ | async"
|
||||
[isLast]="i == (messages$ | async)?.length-1"
|
||||
(emitRead)="markAsRead(m.uuid)"
|
||||
(emitUnread)="markAsUnread(m.uuid)"
|
||||
></ds-message>
|
||||
</ng-container>
|
||||
</ul>
|
||||
<div *ngIf="(messages$ | async)?.length == 0">{{'mydspace.messages.no-messages' | translate}}</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<form *ngVar="(itemUUID$ | async) as itemUuid"
|
||||
[formGroup]="messageForm"
|
||||
(ngSubmit)="sendMessage(itemUuid)">
|
||||
<label *ngIf="!(isSubmitter$ | async)">{{'mydspace.messages.to' | translate}}: <span class="badge badge-pill badge-light">{{(submitter$ | async)?.name}}</span></label>
|
||||
<div class="form-group">
|
||||
<input formControlName="textSubject"
|
||||
class="form-control mb-1"
|
||||
placeholder="{{'mydspace.messages.subject-placeholder' | translate}}"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<textarea formControlName="textDescription"
|
||||
placeholder="{{'mydspace.messages.description-placeholder' | translate}}"
|
||||
class="form-control"
|
||||
rows="3"></textarea>
|
||||
</div>
|
||||
<button id="btn-chat"
|
||||
class="btn btn-warning btn-lg btn-block mt-3"
|
||||
[disabled]="!messageForm.valid || processingMessage"
|
||||
type="submit">
|
||||
<span *ngIf="processingMessage"><i class='fa fa-circle-o-notch fa-spin'></i> {{'mydspace.messages.send-btn' | translate}}</span>
|
||||
<span *ngIf="!processingMessage">{{'mydspace.messages.send-btn' | translate}}</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</ng-template>
|
56
src/app/shared/message-board/message-board.component.scss
Normal file
56
src/app/shared/message-board/message-board.component.scss
Normal file
@@ -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
|
||||
}
|
245
src/app/shared/message-board/message-board.component.ts
Normal file
245
src/app/shared/message-board/message-board.component.ts
Normal file
@@ -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<any>();
|
||||
|
||||
public item$: Observable<Item>;
|
||||
public submitter$: Observable<EPerson>;
|
||||
public user$: Observable<EPerson>;
|
||||
public unreadMessages$: Observable<Bitstream[]> = observableOf([]);
|
||||
public modalRef: NgbModalRef;
|
||||
public itemUUID$: Observable<string>;
|
||||
public messages$: Observable<Bitstream[]> = observableOf([]);
|
||||
public isSubmitter$: Observable<boolean>;
|
||||
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<AppState>,
|
||||
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<Item>) => (rd.hasSucceeded && isNotEmpty(rd.payload))),
|
||||
map((rd: RemoteData<Item>) => rd.payload),
|
||||
tap((u) => console.log(u)));
|
||||
|
||||
this.submitter$ = (this.dso.submitter as Observable<RemoteData<EPerson[]>>).pipe(
|
||||
find((rd: RemoteData<EPerson>) => rd.hasSucceeded && isNotEmpty(rd.payload)),
|
||||
map((rd: RemoteData<EPerson>) => 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<string[]>;
|
||||
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<boolean> {
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
56
src/app/shared/message-board/message/message.component.html
Normal file
56
src/app/shared/message-board/message/message.component.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<li *ngVar="m.findMetadata('dc.type') as type"
|
||||
[ngClass]="{'float-left': (type == 'outbound' && isSubmitter) || (type == 'inbound' && !isSubmitter),
|
||||
'float-right': (type == 'outbound' && !isSubmitter) || (type == 'inbound' && isSubmitter),
|
||||
'clearfix':1}">
|
||||
|
||||
<div class="chat-body clearfix">
|
||||
<div class="header clearfix">
|
||||
|
||||
<small class="float-left">
|
||||
<span class="fa fa-clock text-muted ">{{m.findMetadata('dc.date.issued') | date: 'dd/MM/yyyy HH:mm'}}</span>
|
||||
</small>
|
||||
|
||||
<small
|
||||
*ngIf="showUnread"
|
||||
(click)="markAsUnread()"
|
||||
class="text-muted pointer float-right ml-3">
|
||||
<span class="fa fa-envelope"></span> {{'mydspace.messages.mark-as-unread' | translate}}
|
||||
</small>
|
||||
|
||||
<small
|
||||
*ngIf="showRead"
|
||||
(click)="markAsRead()"
|
||||
class="text-muted pointer float-right ml-3">
|
||||
<span class="fa fa-envelope-open"></span> {{'mydspace.messages.mark-as-read' | translate}}
|
||||
</small>
|
||||
|
||||
<div class="truncatable">
|
||||
<ds-truncatable [id]="m.uuid">
|
||||
<ds-truncatable-part [id]="m.uuid" [minLines]="1">
|
||||
<label>From: <span><strong>{{isSubmitter && type == 'inbound' ? 'You' : m.findMetadata('dc.creator')}}</strong></span></label>
|
||||
</ds-truncatable-part>
|
||||
</ds-truncatable>
|
||||
</div>
|
||||
|
||||
<div class="truncatable" *ngVar="'obj_'.concat(m.uuid) as obj_id">
|
||||
<ds-truncatable [id]="obj_id">
|
||||
<ds-truncatable-part [id]="obj_id" [minLines]="1">
|
||||
<label>Object: <span>{{m.findMetadata('dc.title')}}</span></label>
|
||||
</ds-truncatable-part>
|
||||
</ds-truncatable>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn btn-link" (click)="toggleDescription()">
|
||||
<small *ngIf="showMessage">{{'mydspace.messages.hide-msg' | translate}}</small>
|
||||
<small *ngIf="!showMessage">{{'mydspace.messages.show-msg' | translate}}</small>
|
||||
</button>
|
||||
|
||||
<div *ngIf="showMessage" class="description">
|
||||
{{messageContent | async}}
|
||||
<ds-loading *ngIf="loadingDescription" message="{{'loading.default' | translate}}"></ds-loading>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</li>
|
54
src/app/shared/message-board/message/message.component.scss
Normal file
54
src/app/shared/message-board/message/message.component.scss
Normal file
@@ -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;
|
||||
}
|
83
src/app/shared/message-board/message/message.component.ts
Normal file
83
src/app/shared/message-board/message/message.component.ts
Normal file
@@ -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<any>();
|
||||
@Output() emitRead = new EventEmitter<any>();
|
||||
|
||||
public showUnread: boolean;
|
||||
public showRead: boolean;
|
||||
public showMessage = false;
|
||||
|
||||
private _messageContent: Observable<string> = 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<string> {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user