diff --git a/frontend/app/src/app/modules/profile/id-card.component.ts b/frontend/app/src/app/modules/profile/id-card.component.ts index 72835a4f..b5ebd558 100644 --- a/frontend/app/src/app/modules/profile/id-card.component.ts +++ b/frontend/app/src/app/modules/profile/id-card.component.ts @@ -2,7 +2,7 @@ import {ChangeDetectionStrategy, Component, ElementRef, Input} from '@angular/co import {SCIdCard} from '@openstapps/core'; import {ThingTranslateModule} from '../../translation/thing-translate.module'; import {AsyncPipe, TitleCasePipe} from '@angular/common'; -import {InRangeNowPipe, ToDateRangePipe} from '../../util/in-range.pipe'; +import {IntervalIsNowPipe, ToDateIntervalPipe} from '../../util/in-range.pipe'; import {TranslateModule} from '@ngx-translate/core'; import {AnimationController, ModalController} from '@ionic/angular'; import {ScreenBrightness} from '@capacitor-community/screen-brightness'; @@ -16,7 +16,14 @@ import {iosDuration, iosEasing, mdDuration, mdEasing} from 'src/app/animation/ea styleUrls: ['id-card.scss'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [ThingTranslateModule, InRangeNowPipe, ToDateRangePipe, AsyncPipe, TranslateModule, TitleCasePipe], + imports: [ + ThingTranslateModule, + IntervalIsNowPipe, + ToDateIntervalPipe, + AsyncPipe, + TranslateModule, + TitleCasePipe, + ], }) export class IdCardComponent { @Input({required: true}) item: SCIdCard; diff --git a/frontend/app/src/app/modules/profile/id-card.html b/frontend/app/src/app/modules/profile/id-card.html index 7625cc76..573ab799 100644 --- a/frontend/app/src/app/modules/profile/id-card.html +++ b/frontend/app/src/app/modules/profile/id-card.html @@ -4,6 +4,6 @@ draggable="false" (click)="presentFullscreen()" /> -@if (item.validity && (item.validity | toDateRange | isInRangeNow | async) === false) { +@if (item.validity && (item.validity | rangeToDateInterval | dfnsIntervalIsNow | async) === false) {
{{ 'profile.userInfo.expired' | translate | titlecase }}
} diff --git a/frontend/app/src/app/util/in-range.pipe.ts b/frontend/app/src/app/util/in-range.pipe.ts index 85b99868..093a1660 100644 --- a/frontend/app/src/app/util/in-range.pipe.ts +++ b/frontend/app/src/app/util/in-range.pipe.ts @@ -1,7 +1,7 @@ import {Pipe, PipeTransform} from '@angular/core'; import {SCRange, isInRange, SCISO8601DateRange} from '@openstapps/core'; -import {merge, Observable, timer} from 'rxjs'; -import {distinctUntilChanged, map, startWith} from 'rxjs/operators'; +import {NormalizedInterval, differenceInMilliseconds, interval, isEqual} from 'date-fns'; +import {EMPTY, Observable, SchedulerLike, asyncScheduler, concat, defer, map, of, timer} from 'rxjs'; @Pipe({ name: 'isInRange', @@ -14,28 +14,49 @@ export class InRangePipe implements PipeTransform { } } +export const MIN_DATE = new Date(0); +export const MAX_DATE = new Date(1e15); + @Pipe({ - name: 'toDateRange', + name: 'rangeToDateInterval', pure: true, standalone: true, }) -export class ToDateRangePipe implements PipeTransform { - transform(value: SCISO8601DateRange): SCRange { - return Object.fromEntries(Object.entries(value).map(([key, value]) => [key, new Date(value)])); +export class ToDateIntervalPipe implements PipeTransform { + transform(value: SCISO8601DateRange): NormalizedInterval { + return interval(new Date(value.gte ?? value.gt ?? MIN_DATE), new Date(value.lte ?? value.lt ?? MAX_DATE)); } } +/** + * Returns an Observable that will change its value when the current date is within the given interval. + */ +export function isWithinIntervalObservable( + value: NormalizedInterval, + scheduler: SchedulerLike = asyncScheduler, +): Observable { + return defer(() => { + const now = scheduler.now(); + const activate = differenceInMilliseconds(value.start, now); + const deactivate = differenceInMilliseconds(value.end, now); + + return concat( + of(activate <= 0 && deactivate > 0), + activate <= 0 ? EMPTY : timer(value.start, scheduler).pipe(map(() => true)), + isEqual(value.end, MAX_DATE) || deactivate <= 0 + ? EMPTY + : timer(value.end, scheduler).pipe(map(() => false)), + ); + }); +} + @Pipe({ - name: 'isInRangeNow', + name: 'dfnsIntervalIsNow', pure: true, standalone: true, }) -export class InRangeNowPipe implements PipeTransform { - transform(value: SCRange): Observable { - return merge(timer(value.lte || value.lt || 0), timer(value.gte || value.gt || 0)).pipe( - startWith(0), - map(() => isInRange(new Date(), value)), - distinctUntilChanged(), - ); +export class IntervalIsNowPipe implements PipeTransform { + transform(value: NormalizedInterval): Observable { + return isWithinIntervalObservable(value); } } diff --git a/frontend/app/src/app/util/in-range.spec.ts b/frontend/app/src/app/util/in-range.spec.ts new file mode 100644 index 00000000..836717b7 --- /dev/null +++ b/frontend/app/src/app/util/in-range.spec.ts @@ -0,0 +1,42 @@ +import {TestScheduler} from 'rxjs/testing'; +import {MAX_DATE, MIN_DATE, isWithinIntervalObservable} from './in-range.pipe'; +import {interval} from 'date-fns'; + +/** + * Test macro + */ +function test(range: [number | undefined, number | undefined], subscribe: string, expected: string) { + const testScheduler = new TestScheduler((actual, expected) => { + expect(actual).withContext(actual.map(JSON.stringify).join('\n')).toEqual(expected); + }); + + it(`should emit "${expected}" when "${subscribe}" for range ${range[0] ?? ''}..${range[1] ?? ''}`, () => { + testScheduler.run(({expectObservable}) => { + expectObservable( + isWithinIntervalObservable( + interval(new Date(range[0] ?? MIN_DATE), new Date(range[1] ?? MAX_DATE)), + testScheduler, + ), + subscribe, + ).toBe(expected, {t: true, f: false}); + }); + }); +} + +describe('isWithinIntervalObservable', () => { + test([500, undefined], '1s ^', '1s (t|)'); + test([1000, undefined], '500ms ^', '500ms f 499ms (t|)'); + + test([undefined, 500], '1s ^', '1s (f|)'); + test([undefined, 1000], '500ms ^', '500ms t 499ms (f|)'); + + test([1000, 2000], '500ms ^', '500ms f 499ms t 999ms (f|)'); + + test([500, 1000], '1500ms ^', '1500ms (f|)'); + test([500, 1000], '1s ^', '1000ms (f|)'); + test([500, 1000], '999ms ^', '999ms t (f|)'); + test([500, 1000], '500ms ^', '500ms t 499ms (f|)'); + test([500, 1000], '499ms ^', '499ms f t 499ms (f|)'); + + test([500, 1000], '^ 750ms !', 'f 499ms t'); +});