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');
+});