diff --git a/libs/common/src/tools/send/services/send.service.abstraction.ts b/libs/common/src/tools/send/services/send.service.abstraction.ts index 2821d2c95bb..45f623537db 100644 --- a/libs/common/src/tools/send/services/send.service.abstraction.ts +++ b/libs/common/src/tools/send/services/send.service.abstraction.ts @@ -18,7 +18,17 @@ export abstract class SendService { password: string, key?: SymmetricCryptoKey, ) => Promise<[Send, EncArrayBuffer]>; + /** + * @deprecated Do not call this, use the get$ method + */ get: (id: string) => Send; + /** + * Provides a send for a determined id + * updates after a change occurs to the send that matches the id + * @param id The id of the desired send + * @returns An observable that listens to the value of the desired send + */ + get$: (id: string) => Observable; /** * Provides re-encrypted user sends for the key rotation process * @param newUserKey The new user key to use for re-encryption diff --git a/libs/common/src/tools/send/services/send.service.spec.ts b/libs/common/src/tools/send/services/send.service.spec.ts index a25bfc80f21..65bc5b5e353 100644 --- a/libs/common/src/tools/send/services/send.service.spec.ts +++ b/libs/common/src/tools/send/services/send.service.spec.ts @@ -10,6 +10,11 @@ import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { ContainerService } from "../../../platform/services/container.service"; import { UserKey } from "../../../types/key"; +import { SendType } from "../enums/send-type"; +import { SendFileApi } from "../models/api/send-file.api"; +import { SendTextApi } from "../models/api/send-text.api"; +import { SendFileData } from "../models/data/send-file.data"; +import { SendTextData } from "../models/data/send-text.data"; import { SendData } from "../models/data/send.data"; import { Send } from "../models/domain/send"; import { SendView } from "../models/view/send.view"; @@ -67,6 +72,295 @@ describe("SendService", () => { }); }); + describe("get$", () => { + it("exists", async () => { + const result = await firstValueFrom(sendService.get$("1")); + + expect(result).toEqual(send("1", "Test Send")); + }); + + it("does not exist", async () => { + const result = await firstValueFrom(sendService.get$("2")); + + expect(result).toBe(undefined); + }); + + it("updated observable", async () => { + const singleSendObservable = sendService.get$("1"); + const result = await firstValueFrom(singleSendObservable); + expect(result).toEqual(send("1", "Test Send")); + + await sendService.replace({ + "1": sendData("1", "Test Send Updated"), + }); + + const result2 = await firstValueFrom(singleSendObservable); + expect(result2).toEqual(send("1", "Test Send Updated")); + }); + + it("reports a change when name changes on a new send", async () => { + let changed = false; + sendService.get$("1").subscribe(() => { + changed = true; + }); + const sendDataObject = sendData("1", "Test Send 2"); + + //it is immediately called when subscribed, we need to reset the value + changed = false; + await sendService.replace({ + "1": sendDataObject, + "2": sendData("2", "Test Send 2"), + }); + + expect(changed).toEqual(true); + }); + + it("reports a change when notes changes on a new send", async () => { + const sendDataObject = createSendData() as SendData; + + await sendService.replace({ + "1": sendDataObject, + "2": sendData("2", "Test Send 2"), + }); + + let changed = false; + sendService.get$("1").subscribe(() => { + changed = true; + }); + + sendDataObject.notes = "New notes"; + //it is immediately called when subscribed, we need to reset the value + changed = false; + + await sendService.replace({ + "1": sendDataObject, + "2": sendData("2", "Test Send 2"), + }); + + expect(changed).toEqual(true); + }); + + it("reports a change when Text changes on a new send", async () => { + const sendDataObject = createSendData() as SendData; + + await sendService.replace({ + "1": sendDataObject, + "2": sendData("2", "Test Send 2"), + }); + + let changed = false; + sendService.get$("1").subscribe(() => { + changed = true; + }); + + //it is immediately called when subscribed, we need to reset the value + changed = false; + + sendDataObject.text.text = "new text"; + await sendService.replace({ + "1": sendDataObject, + "2": sendData("2", "Test Send 2"), + }); + + expect(changed).toEqual(true); + }); + + it("reports a change when Text is set as null on a new send", async () => { + const sendDataObject = createSendData() as SendData; + + await sendService.replace({ + "1": sendDataObject, + "2": sendData("2", "Test Send 2"), + }); + + let changed = false; + sendService.get$("1").subscribe(() => { + changed = true; + }); + + //it is immediately called when subscribed, we need to reset the value + changed = false; + + sendDataObject.text = null; + await sendService.replace({ + "1": sendDataObject, + "2": sendData("2", "Test Send 2"), + }); + + expect(changed).toEqual(true); + }); + + it("Doesn't reports a change when File changes on a new send", async () => { + const sendDataObject = createSendData({ + type: SendType.File, + file: new SendFileData(new SendFileApi({ FileName: "name of file" })), + }) as SendData; + await sendService.replace({ + "1": sendDataObject, + "2": sendData("2", "Test Send 2"), + }); + + sendDataObject.file = new SendFileData(new SendFileApi({ FileName: "updated name of file" })); + let changed = false; + sendService.get$("1").subscribe(() => { + changed = true; + }); + + //it is immediately called when subscribed, we need to reset the value + changed = false; + + await sendService.replace({ + "1": sendDataObject, + "2": sendData("2", "Test Send 2"), + }); + + expect(changed).toEqual(false); + }); + + it("reports a change when key changes on a new send", async () => { + const sendDataObject = createSendData() as SendData; + + await sendService.replace({ + "1": sendDataObject, + "2": sendData("2", "Test Send 2"), + }); + + let changed = false; + sendService.get$("1").subscribe(() => { + changed = true; + }); + + //it is immediately called when subscribed, we need to reset the value + changed = false; + + sendDataObject.key = "newKey"; + await sendService.replace({ + "1": sendDataObject, + "2": sendData("2", "Test Send 2"), + }); + + expect(changed).toEqual(true); + }); + + it("reports a change when revisionDate changes on a new send", async () => { + const sendDataObject = createSendData() as SendData; + + await sendService.replace({ + "1": sendDataObject, + "2": sendData("2", "Test Send 2"), + }); + + let changed = false; + sendService.get$("1").subscribe(() => { + changed = true; + }); + + //it is immediately called when subscribed, we need to reset the value + changed = false; + + sendDataObject.revisionDate = "2025-04-05"; + await sendService.replace({ + "1": sendDataObject, + "2": sendData("2", "Test Send 2"), + }); + + expect(changed).toEqual(true); + }); + + it("reports a change when a property is set as null on a new send", async () => { + const sendDataObject = createSendData() as SendData; + + await sendService.replace({ + "1": sendDataObject, + "2": sendData("2", "Test Send 2"), + }); + + let changed = false; + sendService.get$("1").subscribe(() => { + changed = true; + }); + + //it is immediately called when subscribed, we need to reset the value + changed = false; + + sendDataObject.name = null; + await sendService.replace({ + "1": sendDataObject, + "2": sendData("2", "Test Send 2"), + }); + + expect(changed).toEqual(true); + }); + + it("does not reports a change when text's text is set as null on a new send and old send and reports a change then new send sets a text", async () => { + const sendDataObject = createSendData({ + text: new SendTextData(new SendTextApi({ Text: null })), + }) as SendData; + + await sendService.replace({ + "1": sendDataObject, + "2": sendData("2", "Test Send 2"), + }); + + let changed = false; + sendService.get$("1").subscribe(() => { + changed = true; + }); + + //it is immediately called when subscribed, we need to reset the value + changed = false; + + await sendService.replace({ + "1": sendDataObject, + "2": sendData("2", "Test Send 2"), + }); + + expect(changed).toEqual(false); + + sendDataObject.text.text = "Asdf"; + await sendService.replace({ + "1": sendDataObject, + "2": sendData("2", "Test Send 2"), + }); + + expect(changed).toEqual(true); + }); + + it("do not reports a change when nothing changes on the observed send", async () => { + let changed = false; + sendService.get$("1").subscribe(() => { + changed = true; + }); + + const sendDataObject = sendData("1", "Test Send"); + + //it is immediately called when subscribed, we need to reset the value + changed = false; + + await sendService.replace({ + "1": sendDataObject, + "2": sendData("3", "Test Send 3"), + }); + + expect(changed).toEqual(false); + }); + + it("reports a change when the observed send is deleted", async () => { + let changed = false; + sendService.get$("1").subscribe(() => { + changed = true; + }); + //it is immediately called when subscribed, we need to reset the value + changed = false; + + await sendService.replace({ + "2": sendData("2", "Test Send 2"), + }); + + expect(changed).toEqual(true); + }); + }); + it("getAll", async () => { const sends = await sendService.getAll(); const send1 = sends[0]; @@ -184,6 +478,33 @@ describe("SendService", () => { return data; } + const defaultSendData: Partial = { + id: "1", + name: "Test Send", + accessId: "123", + type: SendType.Text, + notes: "notes!", + file: null, + text: new SendTextData(new SendTextApi({ Text: "send text" })), + key: "key", + maxAccessCount: 12, + accessCount: 2, + revisionDate: "2024-09-04", + expirationDate: "2024-09-04", + deletionDate: "2024-09-04", + password: "password", + disabled: false, + hideEmail: false, + }; + + function createSendData(value: Partial = {}) { + const testSend: any = {}; + for (const prop in defaultSendData) { + testSend[prop] = value[prop as keyof SendData] ?? defaultSendData[prop as keyof SendData]; + } + return testSend; + } + function sendView(id: string, name: string) { const data = new SendView({} as any); data.id = id; diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 7aa61474280..0002cad3797 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -1,4 +1,4 @@ -import { BehaviorSubject, concatMap } from "rxjs"; +import { BehaviorSubject, Observable, concatMap, distinctUntilChanged, map } from "rxjs"; import { CryptoFunctionService } from "../../../platform/abstractions/crypto-function.service"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; @@ -116,6 +116,68 @@ export class SendService implements InternalSendServiceAbstraction { return sends.find((send) => send.id === id); } + get$(id: string): Observable { + return this.sends$.pipe( + distinctUntilChanged((oldSends, newSends) => { + const oldSend = oldSends.find((oldSend) => oldSend.id === id); + const newSend = newSends.find((newSend) => newSend.id === id); + if (!oldSend || !newSend) { + // If either oldSend or newSend is not found, consider them different + return false; + } + + // Compare each property of the old and new Send objects + const allPropertiesSame = Object.keys(newSend).every((key) => { + if ( + (oldSend[key as keyof Send] != null && newSend[key as keyof Send] === null) || + (oldSend[key as keyof Send] === null && newSend[key as keyof Send] != null) + ) { + // If a key from either old or new send is not found, and the key from the other send has a value, consider them different + return false; + } + + switch (key) { + case "name": + case "notes": + case "key": + if (oldSend[key] === null && newSend[key] === null) { + return true; + } + + return oldSend[key].encryptedString === newSend[key].encryptedString; + case "text": + if (oldSend[key].text == null && newSend[key].text == null) { + return true; + } + if ( + (oldSend[key].text != null && newSend[key].text == null) || + (oldSend[key].text == null && newSend[key].text != null) + ) { + return false; + } + return oldSend[key].text.encryptedString === newSend[key].text.encryptedString; + case "file": + //Files are never updated so never will be changed. + return true; + case "revisionDate": + case "expirationDate": + case "deletionDate": + if (oldSend[key] === null && newSend[key] === null) { + return true; + } + return oldSend[key].getTime() === newSend[key].getTime(); + default: + // For other properties, compare directly + return oldSend[key as keyof Send] === newSend[key as keyof Send]; + } + }); + + return allPropertiesSame; + }), + map((sends) => sends.find((o) => o.id === id)), + ); + } + async getFromState(id: string): Promise { const sends = await this.stateService.getEncryptedSends(); // eslint-disable-next-line