RED-5908: update viewed pages when a change occurs

This commit is contained in:
Dan Percic 2023-01-09 21:58:28 +02:00
parent 341ec38841
commit e69bb94f8f
15 changed files with 159 additions and 104 deletions

View File

@ -86,16 +86,12 @@
<mat-icon svgIcon="red:nav-first"></mat-icon>
</div>
<div class="pages" id="pages">
<redaction-page-indicator
(pageSelected)="pageSelectedByClick($event)"
*ngFor="let pageNumber of displayedPages"
[activeSelection]="pageHasSelection(pageNumber)"
[active]="pageNumber === activeViewerPage"
[number]="pageNumber"
[showDottedIcon]="hasOnlyManualRedactionsAndIsExcluded(pageNumber)"
></redaction-page-indicator>
</div>
<redaction-pages
(click)="pagesPanelActive = true"
[activePage]="activeViewerPage"
[displayedAnnotations]="displayedAnnotations"
[pages]="displayedPages"
></redaction-pages>
<div
(click)="scrollQuickNavLast()"

View File

@ -77,12 +77,6 @@
}
}
}
.pages {
@include common-mixins.no-scroll-bar;
overflow: auto;
flex: 1;
}
}
.annotations {

View File

@ -185,15 +185,6 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnDestroy
}
}
hasOnlyManualRedactionsAndIsExcluded(pageNumber: number): boolean {
const hasOnlyManualRedactions = this.displayedAnnotations.get(pageNumber)?.every(annotation => annotation.manual);
return hasOnlyManualRedactions && this.file.excludedPages.includes(pageNumber);
}
pageHasSelection(page: number) {
return this.multiSelectService.isActive && !!this.listingService.selected.find(a => a.pageNumber === page);
}
selectAllOnActivePage() {
this.listingService.selectAnnotations(this.activeAnnotations);
}
@ -286,11 +277,6 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnDestroy
return firstValueFrom(this.state.file$).then(file => this.pdf.navigateTo(file.numberOfPages));
}
pageSelectedByClick($event: number): void {
this.pagesPanelActive = true;
this.pdf.navigateTo($event);
}
preventKeyDefault($event: KeyboardEvent): void {
if (COMMAND_KEY_ARRAY.includes($event.key) && !(($event.target as any).localName === 'input')) {
$event.preventDefault();

View File

@ -1,59 +1,45 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { ConfigService } from '@services/config.service';
import { ViewedPagesService } from '@services/files/viewed-pages.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { PageRotationService } from '../../../pdf-viewer/services/page-rotation.service';
import { ContextComponent } from '@iqser/common-ui';
import { ContextComponent, getConfig } from '@iqser/common-ui';
import { tap } from 'rxjs/operators';
import { FileDataService } from '../../services/file-data.service';
import { AppConfig, ViewedPage } from '@red/domain';
import { ViewedPagesMapService } from '@services/files/viewed-pages-map.service';
interface PageIndicatorContext {
isRotated: boolean;
}
@Component({
selector: 'redaction-page-indicator',
selector: 'redaction-page-indicator [number] [read]',
templateUrl: './page-indicator.component.html',
styleUrls: ['./page-indicator.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PageIndicatorComponent extends ContextComponent<PageIndicatorContext> implements OnChanges, OnInit {
@Input() active = false;
@Input() showDottedIcon = false;
@Input() number: number;
@Input() activeSelection = false;
@Input() read = false;
@Output() readonly pageSelected = new EventEmitter<number>();
pageReadTimeout: number = null;
read = false;
isRotated = false;
readonly #config = getConfig<AppConfig>();
constructor(
private readonly _viewedPagesService: ViewedPagesService,
private readonly _configService: ConfigService,
private readonly _viewedPagesMapService: ViewedPagesMapService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _permissionService: PermissionsService,
private readonly _stateService: FilePreviewStateService,
private readonly _fileDataService: FileDataService,
private readonly _state: FilePreviewStateService,
readonly pageRotationService: PageRotationService,
) {
super();
}
get activePage() {
return this._fileDataService.viewedPages.find(p => p.page === this.number);
}
get dossierId() {
return this._stateService.dossierId;
}
get fileId() {
return this._stateService.fileId;
}
ngOnInit() {
const isRotated$ = this.pageRotationService.isRotated$(this.number).pipe(
tap(value => {
@ -65,24 +51,21 @@ export class PageIndicatorComponent extends ContextComponent<PageIndicatorContex
}
ngOnChanges() {
this._setReadState();
this.handlePageRead();
}
async toggleReadState() {
const file = this._stateService.file;
if (this._permissionService.canMarkPagesAsViewed(file)) {
if (this._permissionService.canMarkPagesAsViewed(this._state.file)) {
if (this.read) {
await this._markPageUnread();
await this.#markPageUnread();
} else {
await this._markPageRead();
await this.#markPageRead();
}
}
}
handlePageRead(): void {
const file = this._stateService.file;
if (!this._permissionService.canMarkPagesAsViewed(file)) {
if (!this._permissionService.canMarkPagesAsViewed(this._state.file)) {
return;
}
@ -93,36 +76,22 @@ export class PageIndicatorComponent extends ContextComponent<PageIndicatorContex
if (this.active && !this.read) {
this.pageReadTimeout = window.setTimeout(async () => {
if (this.active && !this.read) {
await this._markPageRead();
await this.#markPageRead();
}
}, this._configService.values.AUTO_READ_TIME * 1000);
}, this.#config.AUTO_READ_TIME * 1000);
}
}
private _setReadState() {
const activePage = this.activePage;
if (!activePage) {
this.read = false;
} else {
this.read = !activePage.showAsUnseen;
}
this._changeDetectorRef.markForCheck();
async #markPageRead() {
const fileId = this._state.fileId;
await this._viewedPagesService.add({ page: this.number }, this._state.dossierId, fileId);
const viewedPage = new ViewedPage({ page: this.number, fileId });
this._viewedPagesMapService.add(fileId, viewedPage);
}
private async _markPageRead() {
await this._viewedPagesService.addPage({ page: this.number }, this.dossierId, this.fileId);
if (this.activePage) {
this.activePage.showAsUnseen = false;
} else {
this._fileDataService.viewedPages.push({ page: this.number, fileId: this.fileId });
}
this._setReadState();
}
private async _markPageUnread() {
await this._viewedPagesService.removePage(this.dossierId, this.fileId, this.number);
const pageToDelete = this._fileDataService.viewedPages.findIndex(p => p.page === this.number);
this._fileDataService.viewedPages.splice(pageToDelete, 1);
this._setReadState();
async #markPageUnread() {
const fileId = this._state.fileId;
await this._viewedPagesService.remove(this._state.dossierId, fileId, this.number);
this._viewedPagesMapService.delete(fileId, this.number);
}
}

View File

@ -0,0 +1,11 @@
<div *ngIf="viewedPages$ | async as viewedPages" class="pages" id="pages">
<redaction-page-indicator
(pageSelected)="pageSelectedByClick($event)"
*ngFor="let pageNumber of pages; trackBy: trackBy"
[activeSelection]="pageHasSelection(pageNumber)"
[active]="pageNumber === activePage"
[number]="pageNumber"
[read]="!!getViewedPage(viewedPages, pageNumber)"
[showDottedIcon]="hasOnlyManualRedactionsAndIsExcluded(pageNumber)"
></redaction-page-indicator>
</div>

View File

@ -0,0 +1,11 @@
@use 'common-mixins';
:host {
display: contents;
}
.pages {
@include common-mixins.no-scroll-bar;
overflow: auto;
flex: 1;
}

View File

@ -0,0 +1,46 @@
import { ChangeDetectionStrategy, Component, inject, Input } from '@angular/core';
import { List } from '@iqser/common-ui';
import { PdfViewer } from '../../../pdf-viewer/services/pdf-viewer.service';
import { MultiSelectService } from '../../services/multi-select.service';
import { AnnotationsListingService } from '../../services/annotations-listing.service';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { ViewedPagesMapService } from '@services/files/viewed-pages-map.service';
import { ViewedPage } from '@red/domain';
@Component({
selector: 'redaction-pages [pages] [activePage] [displayedAnnotations]',
templateUrl: './pages.component.html',
styleUrls: ['./pages.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PagesComponent {
@Input() pages: List<number>;
@Input() activePage: number;
@Input() displayedAnnotations: Map<number, AnnotationWrapper[]>;
readonly #pdf = inject(PdfViewer);
readonly #state = inject(FilePreviewStateService);
readonly viewedPages$ = inject(ViewedPagesMapService).get$(this.#state.fileId);
readonly #multiSelectService = inject(MultiSelectService);
readonly #listingService = inject(AnnotationsListingService);
pageSelectedByClick($event: number): void {
this.#pdf.navigateTo($event);
}
readonly trackBy = (_index: number, item: number) => item;
pageHasSelection(page: number) {
return this.#multiSelectService.isActive && !!this.#listingService.selected.find(a => a.pageNumber === page);
}
hasOnlyManualRedactionsAndIsExcluded(pageNumber: number): boolean {
const hasOnlyManualRedactions = this.displayedAnnotations.get(pageNumber)?.every(annotation => annotation.manual);
return hasOnlyManualRedactions && this.#state.file.excludedPages.includes(pageNumber);
}
getViewedPage(viewedPages: ViewedPage[], pageNumber: number) {
return viewedPages.find(p => p.page === pageNumber);
}
}

View File

@ -54,6 +54,7 @@ import { FilePreviewRightContainerComponent } from './components/right-container
import { RssDialogComponent } from './dialogs/rss-dialog/rss-dialog.component';
import { ReadonlyBannerComponent } from './components/readonly-banner/readonly-banner.component';
import { SuggestionsService } from './services/suggestions.service';
import { PagesComponent } from './components/pages/pages.component';
const routes: Routes = [
{
@ -85,6 +86,7 @@ const components = [
AnnotationWrapperComponent,
AnnotationsListComponent,
PageIndicatorComponent,
PagesComponent,
PageExclusionComponent,
AnnotationActionsComponent,
CommentsComponent,

View File

@ -6,13 +6,15 @@ import { annotationTypesTranslations } from '@translations/annotation-types-tran
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { annotationDefaultColorConfig } from '@red/domain';
import { FilePreviewStateService } from './file-preview-state.service';
import { FileDataService } from './file-data.service';
import { DefaultColorsService } from '@services/entity-services/default-colors.service';
import { of } from 'rxjs';
import { ViewedPagesMapService } from '@services/files/viewed-pages-map.service';
import { FileDataService } from './file-data.service';
@Injectable()
export class AnnotationProcessingService {
constructor(
private readonly _viewedPagesMapService: ViewedPagesMapService,
private readonly _fileDataService: FileDataService,
private readonly _state: FilePreviewStateService,
private readonly _defaultColorsService: DefaultColorsService,
@ -43,7 +45,7 @@ export class AnnotationProcessingService {
checked: false,
topLevelFilter: true,
checker: (annotation: AnnotationWrapper) =>
!this._fileDataService.viewedPages.some(page => page.page === annotation.pageNumber),
!this._viewedPagesMapService.get(this._state.fileId).some(page => page.page === annotation.pageNumber),
},
{
id: 'pages-without-annotations',

View File

@ -1,4 +1,4 @@
import { ChangeType, File, IRedactionLog, IRedactionLogEntry, IViewedPage, ManualRedactionType, ViewMode, ViewModes } from '@red/domain';
import { ChangeType, File, IRedactionLog, IRedactionLogEntry, ManualRedactionType, ViewMode, ViewModes } from '@red/domain';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { BehaviorSubject, firstValueFrom, iif, Observable, Subject } from 'rxjs';
import { RedactionLogEntry } from '@models/file/redaction-log.entry';
@ -20,12 +20,12 @@ import { FilesService } from '@services/files/files.service';
import { DefaultColorsService } from '@services/entity-services/default-colors.service';
import { DictionaryService } from '@services/entity-services/dictionary.service';
import { SuggestionsService } from './suggestions.service';
import { ViewedPagesMapService } from '@services/files/viewed-pages-map.service';
const DELTA_VIEW_TIME = 10 * 60 * 1000; // 10 minutes;
@Injectable()
export class FileDataService extends EntitiesService<AnnotationWrapper, AnnotationWrapper> {
viewedPages: IViewedPage[] = [];
missingTypes = new Set<string>();
readonly hasChangeLog$ = new BehaviorSubject<boolean>(false);
readonly annotations$: Observable<AnnotationWrapper[]>;
@ -37,6 +37,7 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
constructor(
private readonly _state: FilePreviewStateService,
private readonly _viewedPagesService: ViewedPagesService,
private readonly _viewedPagesMapService: ViewedPagesMapService,
private readonly _viewModeService: ViewModeService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _dictionaryService: DictionaryService,
@ -148,11 +149,12 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
async #loadViewedPages(file: File) {
if (!this._permissionsService.canMarkPagesAsViewed(file)) {
this.viewedPages = [];
this._viewedPagesMapService.set(file.fileId, []);
return;
}
this.viewedPages = await this._viewedPagesService.getViewedPages(file.dossierId, file.fileId);
const viewedPages = await this._viewedPagesService.load(file.dossierId, file.fileId);
this._viewedPagesMapService.set(file.fileId, viewedPages);
}
#getVisibleAnnotations(annotations: AnnotationWrapper[], viewMode: ViewMode) {
@ -268,8 +270,7 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
const lastChange = viableChanges.length >= 1 ? viableChanges[viableChanges.length - 1] : undefined;
const page = redactionLogEntry.positions?.[0].page;
const viewedPage = this.viewedPages.filter(p => p.page === page).pop();
const viewedPage = this._viewedPagesMapService.get(file.fileId, page);
// page has been seen -> let's see if it's a change
if (viewedPage) {
@ -279,7 +280,11 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
// at least one unseen change
if (relevantChanges.length > 0) {
// at least 1 relevant change
viewedPage.showAsUnseen = dayjs(viewedPage.viewedTime).valueOf() < dayjs(lastChange.dateTime).valueOf();
const showAsUnseen = dayjs(viewedPage.viewedTime).valueOf() < dayjs(lastChange.dateTime).valueOf();
if (showAsUnseen) {
this._viewedPagesMapService.delete(this._state.fileId, viewedPage);
}
this.hasChangeLog$.next(true);
return {
changeLogType: relevantChanges[relevantChanges.length - 1].type,

View File

@ -0,0 +1,8 @@
import { Injectable } from '@angular/core';
import { IViewedPage, ViewedPage } from '@red/domain';
import { EntitiesMapService } from '@iqser/common-ui';
@Injectable({ providedIn: 'root' })
export class ViewedPagesMapService extends EntitiesMapService<IViewedPage, ViewedPage> {
protected readonly _primaryKey = 'id';
}

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { GenericService, RequiredParam, Validate } from '@iqser/common-ui';
import { GenericService, mapEach, RequiredParam, Validate } from '@iqser/common-ui';
import { catchError, map } from 'rxjs/operators';
import { IViewedPage, IViewedPagesRequest } from '@red/domain';
import { IViewedPage, IViewedPagesRequest, ViewedPage } from '@red/domain';
import { firstValueFrom, of } from 'rxjs';
@Injectable({
@ -11,22 +11,23 @@ export class ViewedPagesService extends GenericService<unknown> {
protected readonly _defaultModelPath = 'viewedPages';
@Validate()
addPage(@RequiredParam() body: IViewedPagesRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) {
add(@RequiredParam() body: IViewedPagesRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) {
const modelPath = `${this._defaultModelPath}/${dossierId}/${fileId}`;
return firstValueFrom(this._post(body, modelPath));
}
@Validate()
removePage(@RequiredParam() dossierId: string, @RequiredParam() fileId: string, @RequiredParam() page: number) {
remove(@RequiredParam() dossierId: string, @RequiredParam() fileId: string, @RequiredParam() page: number) {
const modelPath = `${this._defaultModelPath}/${dossierId}/${fileId}/${page}`;
return firstValueFrom(super.delete({}, modelPath));
}
@Validate()
getViewedPages(@RequiredParam() dossierId: string, @RequiredParam() fileId: string) {
load(@RequiredParam() dossierId: string, @RequiredParam() fileId: string) {
const request = this._getOne<{ pages?: IViewedPage[] }>([dossierId, fileId]).pipe(
map(res => res.pages),
catchError(() => of([] as IViewedPage[])),
mapEach(page => new ViewedPage(page)),
);
return firstValueFrom(request);

View File

@ -1,5 +1,6 @@
export * from './page-range';
export * from './viewed-page';
export * from './viewed-page.model';
export * from './viewed-pages.request';
export * from './page-exclusion.request';
export * from './page-rotation.request';

View File

@ -0,0 +1,24 @@
import { Entity } from '@iqser/common-ui';
import { IViewedPage } from '.';
export class ViewedPage extends Entity<IViewedPage, number> {
readonly fileId: string;
readonly page: number;
readonly userId?: string;
readonly viewedTime?: string;
override readonly routerLink = undefined;
override readonly searchKey = '';
constructor(viewedPage: IViewedPage) {
super(viewedPage);
this.fileId = viewedPage.fileId;
this.page = viewedPage.page;
this.userId = viewedPage.userId;
this.viewedTime = viewedPage.viewedTime;
}
override get id() {
return this.page;
}
}

View File

@ -1,7 +1,6 @@
export interface IViewedPage {
fileId?: string;
page?: number;
userId?: string;
viewedTime?: string;
showAsUnseen?: boolean;
readonly fileId: string;
readonly page: number;
readonly userId?: string;
readonly viewedTime?: string;
}