mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 05:30:01 +00:00
wire next achievement service to notifier service
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { firstValueFrom, switchMap, tap } from "rxjs";
|
||||
import { concat, filter, map, mergeAll, switchMap } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { AchievementService } from "@bitwarden/common/tools/achievements/achievement.service.abstraction";
|
||||
import { NextAchievementService } from "@bitwarden/common/tools/achievements/next-achievement.service";
|
||||
import { Achievement } from "@bitwarden/common/tools/achievements/types";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { Icon, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { AchievementNotifierService as AchievementNotifierServiceAbstraction } from "./achievement-notifier.abstraction";
|
||||
@@ -13,7 +15,7 @@ import { iconMap } from "./icons/iconMap";
|
||||
export class AchievementNotifierService implements AchievementNotifierServiceAbstraction {
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private achievementService: AchievementService,
|
||||
private achievementService: NextAchievementService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private toastService: ToastService,
|
||||
@@ -24,56 +26,32 @@ export class AchievementNotifierService implements AchievementNotifierServiceAbs
|
||||
}
|
||||
|
||||
private async setupListeners() {
|
||||
// FIXME Implement achievements earned filter and notififer
|
||||
/* Get the userId from the accountService
|
||||
* Subscribe to achievementService.achievementsEarned$(userId)
|
||||
* Retrieve current device and filter out messages that are not for this client/device (achievements should be only shown on the device that earned them)
|
||||
* Retrieve Achievement by AchievementId via the achievementService
|
||||
* Use information from Achievement to fill out the options for the notification (toast)
|
||||
* Invoke showing toast
|
||||
*/
|
||||
// FIXME getClientType browswer and achievementEarned.service.name.extension won't match
|
||||
const account = await firstValueFrom(this.accountService.activeAccount$);
|
||||
this.achievementService
|
||||
.achievementsEarned$(account.id)
|
||||
this.accountService.accounts$
|
||||
.pipe(
|
||||
// Removing filter for testing purposes
|
||||
// filter(achievementEarned => achievementEarned.service.name == this.platformUtilsService.getClientType())).pipe(
|
||||
switchMap((earned) => this.achievementService.achievementById$(earned.achievement.name)),
|
||||
tap((achievement) => {
|
||||
//eslint-disable-next-line no-console
|
||||
console.log(achievement);
|
||||
switchMap((accounts) => {
|
||||
const earned$ = Array.from(Object.entries(accounts), ([id, value]) => {
|
||||
const account = { ...value, id: id as UserId };
|
||||
const metadata = this.achievementService.achievementMap();
|
||||
const achievements = this.achievementService.earnedStream$(account).pipe(
|
||||
map((earned) => metadata.get(earned.achievement.name)),
|
||||
// FIXME: exclude achievements earned on another device
|
||||
filter((earned): earned is Achievement => !!earned),
|
||||
);
|
||||
|
||||
return achievements;
|
||||
});
|
||||
return concat(earned$);
|
||||
}),
|
||||
mergeAll(),
|
||||
)
|
||||
.subscribe((achievement) => {
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
title: achievement.name,
|
||||
message: achievement.description,
|
||||
message: achievement.description ?? "",
|
||||
icon: this.lookupIcon(achievement.achievement),
|
||||
});
|
||||
});
|
||||
|
||||
// FIXME Migrate to use achievementHub.earned$() instead of achievementService.achievementsEarned$
|
||||
// this.achievementService
|
||||
// .earned$
|
||||
// .pipe(
|
||||
// // Removing filter for testing purposes
|
||||
// // filter(achievementEarned => achievementEarned.service.name == this.platformUtilsService.getClientType())).pipe(
|
||||
// switchMap((earned) => this.achievementService.achievementById$(earned.achievement.name)),
|
||||
// tap((achievement) => {
|
||||
// //eslint-disable-next-line no-console
|
||||
// console.log(achievement);
|
||||
// }),
|
||||
// )
|
||||
// .subscribe((achievement) => {
|
||||
// this.toastService.showToast({
|
||||
// variant: "info",
|
||||
// title: achievement.name,
|
||||
// message: achievement.description,
|
||||
// icon: this.lookupIcon(achievement.name),
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
lookupIcon(achievementName: string): Icon {
|
||||
|
||||
@@ -82,8 +82,7 @@ export class AchievementHub {
|
||||
|
||||
earned$(): Observable<Map<AchievementId, AchievementEarnedEvent>> {
|
||||
return this.achievementLog.pipe(
|
||||
filter((e) => isEarnedEvent(e)),
|
||||
map((e) => e as AchievementEarnedEvent),
|
||||
filter(isEarnedEvent),
|
||||
latestEarnedMetrics(),
|
||||
tap((m) => this.log.debug(m, "earned achievements update")),
|
||||
startWith(new Map<AchievementId, AchievementEarnedEvent>()),
|
||||
@@ -92,8 +91,7 @@ export class AchievementHub {
|
||||
|
||||
metrics$(): Observable<Map<MetricId, AchievementProgressEvent>> {
|
||||
return this.achievementLog.pipe(
|
||||
filter((e) => isProgressEvent(e)),
|
||||
map((e) => e as AchievementProgressEvent),
|
||||
filter(isProgressEvent),
|
||||
latestProgressMetrics(),
|
||||
tap((m) => this.log.debug(m, "achievement metrics update")),
|
||||
startWith(new Map<MetricId, AchievementProgressEvent>()),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Observable, OperatorFunction, map, pipe, withLatestFrom } from "rxjs";
|
||||
import { Observable, OperatorFunction, combineLatestWith, map, pipe, withLatestFrom } from "rxjs";
|
||||
|
||||
import { AchievementId, AchievementValidator, MetricId } from "./types";
|
||||
|
||||
@@ -7,12 +7,15 @@ import { AchievementId, AchievementValidator, MetricId } from "./types";
|
||||
function active(
|
||||
metrics$: Observable<ReadonlyMap<MetricId, number>>,
|
||||
earned$: Observable<ReadonlySet<AchievementId>>,
|
||||
// TODO: accept a configuration observable that completes without
|
||||
// emission when the user has opted out of achievements
|
||||
): OperatorFunction<AchievementValidator[], AchievementValidator[]> {
|
||||
return pipe(
|
||||
// TODO: accept a configuration observable that completes without
|
||||
// emission when the user has opted out of achievements
|
||||
withLatestFrom(metrics$, earned$),
|
||||
map(([monitors, metrics, earned]) => {
|
||||
// refresh when an achievement is earned, but not when metrics
|
||||
// update; this may cause metrics to overrun
|
||||
withLatestFrom(metrics$),
|
||||
combineLatestWith(earned$),
|
||||
map(([[monitors, metrics], earned]) => {
|
||||
// compute list of active achievements
|
||||
const active = monitors.filter((m) => {
|
||||
// 🧠 the filters could be lifted into a function argument & delivered
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { filter, find, from, map, Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { AchievementHub } from "./achievement-hub";
|
||||
import { AchievementService as AchievementServiceAbstraction } from "./achievement.service.abstraction";
|
||||
import { EventStoreAbstraction } from "./event-store.abstraction.service";
|
||||
import {
|
||||
VaultItems_1_Added_Achievement,
|
||||
VaultItems_10_Added_Achievement,
|
||||
} from "./examples/achievements";
|
||||
import { isEarnedEvent, isProgressEvent } from "./meta";
|
||||
import {
|
||||
Achievement,
|
||||
AchievementEarnedEvent,
|
||||
AchievementEvent,
|
||||
AchievementId,
|
||||
AchievementProgressEvent,
|
||||
} from "./types";
|
||||
|
||||
// Service might be deprecated in favor of the AchievmentHub
|
||||
// The hub is currently missing a way of listing all achievements, finding by id, but that could be possibly done via the AchievementManager
|
||||
export class HubAchievementService implements AchievementServiceAbstraction {
|
||||
private _achievements: Achievement[] = [
|
||||
VaultItems_1_Added_Achievement,
|
||||
VaultItems_10_Added_Achievement,
|
||||
];
|
||||
|
||||
private _achievementsSubject = from(this._achievements);
|
||||
|
||||
earned$: Observable<AchievementEarnedEvent>;
|
||||
inProgress$: Observable<AchievementProgressEvent>;
|
||||
|
||||
achievementById$: (achievementId: string) => Observable<Achievement>;
|
||||
achievementsEarned$ = (userId: UserId) => { return this.earned$ };
|
||||
achievementsInProgress$ = (userId: UserId) => { return this.inProgress$ }
|
||||
|
||||
private achievementHub = new AchievementHub();
|
||||
|
||||
constructor() {
|
||||
this.achievementById$ = (achievementId: AchievementId) =>
|
||||
this._achievementsSubject.pipe(find((item: Achievement) => item.name === achievementId));
|
||||
|
||||
this.earned$ = this.achievementHub.new$().pipe(filter((event) => isEarnedEvent(event)), map((event) => {
|
||||
return event as AchievementEarnedEvent;
|
||||
}));
|
||||
|
||||
this.inProgress$ = this.achievementHub.new$().pipe(filter((event) => isProgressEvent(event)), map((event) => {
|
||||
return event as AchievementProgressEvent;
|
||||
}));
|
||||
}
|
||||
}
|
||||
100
libs/common/src/tools/achievements/next-achievement.service.ts
Normal file
100
libs/common/src/tools/achievements/next-achievement.service.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { BehaviorSubject, EMPTY, filter, find, from, Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { Account } from "../../auth/abstractions/account.service";
|
||||
import { UserEventLogProvider } from "../log/logger";
|
||||
|
||||
import { AchievementHub } from "./achievement-hub";
|
||||
import { AchievementService as AchievementServiceAbstraction } from "./achievement.service.abstraction";
|
||||
import { isEarnedEvent, isProgressEvent } from "./meta";
|
||||
import {
|
||||
Achievement,
|
||||
AchievementEarnedEvent,
|
||||
AchievementEvent,
|
||||
AchievementProgressEvent,
|
||||
AchievementValidator,
|
||||
} from "./types";
|
||||
import { ItemCreatedCountConfig } from "./validators/config/item-created-count-config";
|
||||
import { SendItemCreatedCountConfig } from "./validators/config/send-created-count-config";
|
||||
import { SendItemCreatedCountValidator } from "./validators/send-item-created-count-validator";
|
||||
import { VaultItemCreatedCountValidator } from "./validators/vault-item-created-count-validator";
|
||||
|
||||
export class NextAchievementService implements AchievementServiceAbstraction {
|
||||
constructor(private readonly eventLogs: UserEventLogProvider) {}
|
||||
|
||||
private hubs = new Map<string, AchievementHub>();
|
||||
|
||||
private getHub(account: Account) {
|
||||
if (!this.hubs.has(account.id)) {
|
||||
// FIXME: sync these from the server and load them
|
||||
const validators$ = new BehaviorSubject<AchievementValidator[]>([
|
||||
...VaultItemCreatedCountValidator.createValidators(ItemCreatedCountConfig.AllConfigs),
|
||||
...SendItemCreatedCountValidator.createValidators(SendItemCreatedCountConfig.AllConfigs),
|
||||
]);
|
||||
|
||||
// FIXME: load stored achievements
|
||||
const achievements$ = from([] as AchievementEvent[]);
|
||||
const events$ = this.eventLogs.monitor$(account);
|
||||
const hub = new AchievementHub(validators$, events$, achievements$);
|
||||
|
||||
this.hubs.set(account.id, hub);
|
||||
}
|
||||
|
||||
return this.hubs.get(account.id)!;
|
||||
}
|
||||
|
||||
private _achievements: Achievement[] = [
|
||||
...ItemCreatedCountConfig.AllConfigs,
|
||||
...SendItemCreatedCountConfig.AllConfigs,
|
||||
];
|
||||
|
||||
private _achievementsSubject = from(this._achievements);
|
||||
|
||||
achievementMap() {
|
||||
return new Map(this._achievements.map((a) => [a.achievement, a] as const));
|
||||
}
|
||||
|
||||
earnedStream$(account: Account, all: boolean = false) {
|
||||
const hub = this.getHub(account);
|
||||
if (all) {
|
||||
return hub.all$().pipe(filter(isEarnedEvent));
|
||||
} else {
|
||||
return hub.new$().pipe(filter(isEarnedEvent));
|
||||
}
|
||||
}
|
||||
|
||||
earnedMap$(account: Account) {
|
||||
return this.getHub(account).metrics$();
|
||||
}
|
||||
|
||||
progressStream$(account: Account, all: boolean = false) {
|
||||
const hub = this.getHub(account);
|
||||
if (all) {
|
||||
return hub.all$().pipe(filter(isProgressEvent));
|
||||
} else {
|
||||
return hub.new$().pipe(filter(isProgressEvent));
|
||||
}
|
||||
}
|
||||
|
||||
metricsMap$(account: Account) {
|
||||
return this.getHub(account).metrics$();
|
||||
}
|
||||
|
||||
achievementById$(achievementId: string): Observable<Achievement> {
|
||||
return this._achievementsSubject.pipe(
|
||||
find((item: Achievement) => item.name === achievementId),
|
||||
filter((f): f is Achievement => !!f),
|
||||
);
|
||||
}
|
||||
|
||||
earned$: Observable<AchievementEarnedEvent> = EMPTY;
|
||||
inProgress$: Observable<AchievementProgressEvent> = EMPTY;
|
||||
achievementsEarned$(userId: UserId): Observable<AchievementEarnedEvent> {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
achievementsInProgress$(userId: UserId): Observable<AchievementProgressEvent> {
|
||||
return EMPTY;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BehaviorSubject, SubjectLike, from, map, zip } from "rxjs";
|
||||
import { BehaviorSubject, Observable, SubjectLike, from, map, zip } from "rxjs";
|
||||
import { Primitive } from "type-fest";
|
||||
|
||||
import { Account } from "../../auth/abstractions/account.service";
|
||||
@@ -11,7 +11,8 @@ import { disabledSemanticLoggerProvider } from "./factory";
|
||||
import { SemanticLogger } from "./semantic-logger.abstraction";
|
||||
|
||||
export abstract class UserEventLogProvider {
|
||||
abstract create: (account: Account) => UserEventLogger;
|
||||
abstract capture: (account: Account) => UserEventLogger;
|
||||
abstract monitor$: (account: Account) => Observable<UserActionEvent>;
|
||||
}
|
||||
|
||||
type BaselineType = Omit<ServiceFormat & UserFormat, "@timestamp">;
|
||||
|
||||
Reference in New Issue
Block a user