diff --git a/.github/workflows/cherry-pick-release-to-dev.yml b/.github/workflows/cherry-pick-release-to-dev.yml new file mode 100644 index 00000000000..8330dd64e1b --- /dev/null +++ b/.github/workflows/cherry-pick-release-to-dev.yml @@ -0,0 +1,20 @@ +# This job will automatically create a cherry pick PR from any release/* branch to the dev branch +# It allows the dev branch to stay up-to-date with fixes made to specific release branches +name: Cherry pick to dev +on: + push: + branches: + - release/* +jobs: + cherry_pick: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: Create PR to branch + uses: gorillio/github-action-cherry-pick@master + with: + pr_branch: 'dev' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db605f12aee..a998bd4e05e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,8 +20,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/create_docker_image.yml b/.github/workflows/create_docker_image.yml index 9a3573ae8ac..3398579c70a 100644 --- a/.github/workflows/create_docker_image.yml +++ b/.github/workflows/create_docker_image.yml @@ -25,8 +25,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/deploy-to-test-env.yml b/.github/workflows/deploy-to-test-env.yml index 0dc92013697..a7a8c4b0cd2 100644 --- a/.github/workflows/deploy-to-test-env.yml +++ b/.github/workflows/deploy-to-test-env.yml @@ -32,8 +32,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/src/i18n/en-US.json b/src/i18n/en-US.json index af5104d4f68..a388094f3a7 100644 --- a/src/i18n/en-US.json +++ b/src/i18n/en-US.json @@ -503,6 +503,8 @@ "conversationNewConversation": "Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and your contact.", "conversationNotClassified": "Security level: Unclassified", "conversationNotFoundMessage": "You may not have permission with this account or it no longer exists.", + "conversationWithBlockedUserMessage": "The link to this group conversation is no longer valid or leads to a conversation with someone you blocked.", + "conversationWithBlockedUserTitle": "Conversation not reachable", "conversationNotFoundTitle": "{{brandName}} can’t open this conversation.", "conversationParticipantsSearchPlaceholder": "Search by name", "conversationParticipantsTitle": "People", diff --git a/src/script/E2EIdentity/E2EIdentityEnrollment.test.ts b/src/script/E2EIdentity/E2EIdentityEnrollment.test.ts index a35a62df59d..a3ef57eabb9 100644 --- a/src/script/E2EIdentity/E2EIdentityEnrollment.test.ts +++ b/src/script/E2EIdentity/E2EIdentityEnrollment.test.ts @@ -99,8 +99,6 @@ describe('E2EIHandler', () => { Config.getConfig = jest.fn().mockReturnValue({FEATURE: {ENABLE_E2EI: true}}); jest.spyOn(PrimaryModal, 'show'); - (getModalOptions as jest.Mock).mockClear(); - (getCertificateDetails as jest.Mock).mockClear(); jest .spyOn(container.resolve(UserState), 'self') @@ -110,19 +108,19 @@ describe('E2EIHandler', () => { }); it('should create instance with valid params', async () => { - const instance = await E2EIHandler.getInstance().initialize(params); + const instance = E2EIHandler.getInstance().initialize(params); expect(instance).toBeInstanceOf(E2EIHandler); }); it('should always return the same instance', async () => { - const instance1 = await E2EIHandler.getInstance().initialize(params); - const instance2 = await E2EIHandler.getInstance().initialize(params); + const instance1 = E2EIHandler.getInstance().initialize(params); + const instance2 = E2EIHandler.getInstance().initialize(params); expect(instance1).toBe(instance2); }); it('should set currentStep to INITIALIZE after initialize is called', async () => { const instance = E2EIHandler.getInstance(); - await instance.initialize(params); + instance.initialize(params); void instance.attemptEnrollment(); await wait(1); expect(instance['currentStep']).toBe(E2EIHandlerStep.INITIALIZED); @@ -135,7 +133,7 @@ describe('E2EIHandler', () => { jest.spyOn(container.resolve(Core), 'enrollE2EI').mockResolvedValueOnce(true); - const instance = await E2EIHandler.getInstance().initialize(params); + const instance = E2EIHandler.getInstance().initialize(params); void instance['enroll'](); await wait(1); expect(instance['currentStep']).toBe(E2EIHandlerStep.SUCCESS); @@ -146,14 +144,14 @@ describe('E2EIHandler', () => { jest.spyOn(container.resolve(Core), 'enrollE2EI').mockImplementationOnce(jest.fn(() => Promise.reject())); jest.spyOn(container.resolve(UserState), 'self').mockImplementationOnce(() => user); - const instance = await E2EIHandler.getInstance().initialize(params); + const instance = E2EIHandler.getInstance().initialize(params); void instance['enroll'](); await wait(1); expect(instance['currentStep']).toBe(E2EIHandlerStep.ERROR); }); it('should display user info message when initialized', async () => { - const instance = await E2EIHandler.getInstance().initialize(params); + const instance = E2EIHandler.getInstance().initialize(params); void instance.attemptEnrollment(); await wait(1); expect(getModalOptions).toHaveBeenCalledWith( @@ -170,7 +168,7 @@ describe('E2EIHandler', () => { }); it('should display loading message when enroled', async () => { - const handler = await E2EIHandler.getInstance().initialize(params); + const handler = E2EIHandler.getInstance().initialize(params); void handler['enroll'](); await wait(1); expect(getModalOptions).toHaveBeenCalledWith( @@ -183,7 +181,7 @@ describe('E2EIHandler', () => { it('should display success message when enrollment is done', async () => { jest.spyOn(container.resolve(Core), 'enrollE2EI').mockResolvedValueOnce(true); - const handler = await E2EIHandler.getInstance().initialize(params); + const handler = E2EIHandler.getInstance().initialize(params); handler['showLoadingMessage'] = jest.fn(); void handler['enroll'](); await wait(1); @@ -197,7 +195,7 @@ describe('E2EIHandler', () => { it('should display error message when enrollment fails', async () => { jest.spyOn(container.resolve(Core), 'enrollE2EI').mockRejectedValueOnce(false); - const handler = await E2EIHandler.getInstance().initialize(params); + const handler = E2EIHandler.getInstance().initialize(params); handler['showLoadingMessage'] = jest.fn(); void handler['enroll'](); await wait(1); @@ -220,7 +218,7 @@ describe('E2EIHandler', () => { const renewCertificateSpy = jest.spyOn(handler as any, 'renewCertificate'); // Initialize E2EI - await handler.initialize(params); + handler.initialize(params); void handler.attemptRenewal(); await wait(1); @@ -240,7 +238,7 @@ describe('E2EIHandler', () => { const enrollSpy = jest.spyOn(handler, 'enroll'); // Initialize E2EI - await handler.initialize(params); + handler.initialize(params); void handler.attemptRenewal(); await wait(1); @@ -272,7 +270,7 @@ describe('E2EIHandler', () => { const renewCertificateSpy = jest.spyOn(handler as any, 'renewCertificate'); // Initialize E2EI - await handler.initialize(params); + handler.initialize(params); void handler.attemptRenewal(); await wait(1); @@ -280,22 +278,22 @@ describe('E2EIHandler', () => { expect(renewCertificateSpy).not.toHaveBeenCalled(); }); - it('call showE2EINotificationMessage when no active certificate is found', async () => { + it('call startEnrollment when no active certificate is found', async () => { const handler = E2EIHandler.getInstance(); // Set active certificate to be false (hasActiveCertificate as jest.Mock).mockResolvedValue(false); const renewCertificateSpy = jest.spyOn(handler as any, 'renewCertificate'); - const showE2EINotificationMessageSpy = jest.spyOn(handler as any, 'showE2EINotificationMessage'); + const startEnrollmentSpy = jest.spyOn(handler as any, 'startEnrollment'); // Initialize E2EI - await handler.initialize(params); + handler.initialize(params); void handler.attemptEnrollment(); await wait(1); expect(renewCertificateSpy).not.toHaveBeenCalled(); - expect(showE2EINotificationMessageSpy).toHaveBeenCalled(); + expect(startEnrollmentSpy).toHaveBeenCalled(); }); it('for invalid certificate user can not get another certificate until deleting a client', async () => { @@ -305,7 +303,7 @@ describe('E2EIHandler', () => { (hasActiveCertificate as jest.Mock).mockResolvedValue(true); const renewCertificateSpy = jest.spyOn(handler as any, 'renewCertificate'); - const showE2EINotificationMessageSpy = jest.spyOn(handler as any, 'showE2EINotificationMessage'); + const startEnrollmentSpy = jest.spyOn(handler as any, 'startEnrollment'); const timeRemainingMS = 5 * TimeInMillis.DAY; // 5 days remaining @@ -318,11 +316,11 @@ describe('E2EIHandler', () => { jest.spyOn(handler as any, 'shouldRefresh').mockReturnValue(false); // Initialize E2EI - await handler.initialize(params); + handler.initialize(params); void handler.attemptRenewal(); await wait(1); expect(renewCertificateSpy).not.toHaveBeenCalled(); - expect(showE2EINotificationMessageSpy).not.toHaveBeenCalled(); + expect(startEnrollmentSpy).not.toHaveBeenCalled(); }); }); diff --git a/src/script/E2EIdentity/E2EIdentityEnrollment.ts b/src/script/E2EIdentity/E2EIdentityEnrollment.ts index 1b8e75afaa5..c0469af0c2f 100644 --- a/src/script/E2EIdentity/E2EIdentityEnrollment.ts +++ b/src/script/E2EIdentity/E2EIdentityEnrollment.ts @@ -34,8 +34,6 @@ import {formatDelayTime, TIME_IN_MILLIS} from 'Util/TimeUtil'; import {removeUrlParameters} from 'Util/UrlUtil'; import {supportsMLS} from 'Util/util'; -import {getDelayTime, shouldEnableSoftLock} from './DelayTimer/delay'; -import {DelayTimerService} from './DelayTimer/DelayTimer'; import { hasActiveCertificate, isE2EIEnabled, @@ -46,6 +44,8 @@ import { import {getModalOptions, ModalType} from './Modals'; import {OIDCService} from './OIDCService'; import {OIDCServiceStore} from './OIDCService/OIDCServiceStorage'; +import {getSnoozeTime, shouldEnableSoftLock} from './SnoozableTimer/delay'; +import {SnoozableTimer} from './SnoozableTimer/SnoozableTimer'; import {Config} from '../Config'; @@ -68,7 +68,7 @@ type Events = { }; export type EnrollmentConfig = { - timer: DelayTimerService; + timer: SnoozableTimer; discoveryUrl: string; gracePeriodInMs: number; }; @@ -80,7 +80,7 @@ export class E2EIHandler extends TypedEventEmitter { private readonly core = container.resolve(Core); private readonly userState = container.resolve(UserState); private config?: EnrollmentConfig; - private currentStep: E2EIHandlerStep | null = E2EIHandlerStep.UNINITIALIZED; + private currentStep: E2EIHandlerStep = E2EIHandlerStep.UNINITIALIZED; private oidcService?: OIDCService; private get coreE2EIService() { @@ -126,12 +126,13 @@ export class E2EIHandler extends TypedEventEmitter { this.config = { discoveryUrl, gracePeriodInMs, - timer: new DelayTimerService({ + timer: new SnoozableTimer({ gracePeriodInMS: gracePeriodInMs, - gracePeriodExpiredCallback: () => null, - delayPeriodExpiredCallback: () => null, + onGracePeriodExpired: () => this.startEnrollment(ModalType.ENROLL), + onSnoozeExpired: () => this.startEnrollment(ModalType.ENROLL), }), }; + this.currentStep = E2EIHandlerStep.INITIALIZED; } return this; } @@ -142,7 +143,7 @@ export class E2EIHandler extends TypedEventEmitter { // If the client already has a certificate, we don't need to start the enrollment return; } - return this.showE2EINotificationMessage(ModalType.ENROLL); + return this.startEnrollment(ModalType.ENROLL); } public async attemptRenewal(): Promise { @@ -203,7 +204,7 @@ export class E2EIHandler extends TypedEventEmitter { // If the silent authentication fails, clear the oidc service progress/data and renew manually await this.cleanUp(true); - this.showE2EINotificationMessage(ModalType.CERTIFICATE_RENEWAL); + await this.startEnrollment(ModalType.CERTIFICATE_RENEWAL); } } @@ -377,7 +378,7 @@ export class E2EIHandler extends TypedEventEmitter { resolve(); }, secondaryActionFn: async () => { - await this.showE2EINotificationMessage(ModalType.ENROLL); + await this.startEnrollment(ModalType.ENROLL); resolve(); }, }); @@ -386,33 +387,12 @@ export class E2EIHandler extends TypedEventEmitter { }); } - private shouldShowNotification(): boolean { - // If the user has already snoozed the notification, don't show it again until the snooze period has expired - if (this.currentStep !== E2EIHandlerStep.UNINITIALIZED && this.currentStep !== E2EIHandlerStep.SNOOZE) { - return false; - } - return true; - } - - private initializeEnrollmentTimer(): void { - // Only initialize the timer when the it is uninitialized - if (this.currentStep === E2EIHandlerStep.UNINITIALIZED) { - this.config?.timer.updateParams({ - gracePeriodInMS: this.config.gracePeriodInMs, - gracePeriodExpiredCallback: () => { - this.showE2EINotificationMessage(ModalType.ENROLL); - }, - delayPeriodExpiredCallback: () => { - this.showE2EINotificationMessage(ModalType.ENROLL); - }, - }); - this.currentStep = E2EIHandlerStep.INITIALIZED; - } - } - - private async showEnrollmentModal(modalType: ModalType.ENROLL | ModalType.CERTIFICATE_RENEWAL): Promise { + private async showEnrollmentModal( + modalType: ModalType.ENROLL | ModalType.CERTIFICATE_RENEWAL, + config: EnrollmentConfig, + ): Promise { // Show the modal with the provided modal type - const disableSnooze = await shouldEnableSoftLock(this.config!); + const disableSnooze = await shouldEnableSoftLock(config); return new Promise(resolve => { const {modalOptions, modalType: determinedModalType} = getModalOptions({ hideSecondary: disableSnooze, @@ -422,8 +402,8 @@ export class E2EIHandler extends TypedEventEmitter { }, secondaryActionFn: () => { this.currentStep = E2EIHandlerStep.SNOOZE; - this.config?.timer.delayPrompt(); - this.showSnoozeModal(); + this.config?.timer.snooze(); + this.showSnoozeConfirmationModal(); resolve(); }, type: modalType, @@ -433,38 +413,33 @@ export class E2EIHandler extends TypedEventEmitter { }); } - private showSnoozeModal() { + private showSnoozeConfirmationModal() { // Show the modal with the provided modal type const {modalOptions, modalType: determinedModalType} = getModalOptions({ type: ModalType.SNOOZE_REMINDER, hideClose: true, extraParams: { - delayTime: formatDelayTime(getDelayTime(this.config!.gracePeriodInMs)), + delayTime: formatDelayTime(getSnoozeTime(this.config!.gracePeriodInMs)), }, }); PrimaryModal.show(determinedModalType, modalOptions); } - public async showE2EINotificationMessage( - modalType: ModalType.CERTIFICATE_RENEWAL | ModalType.ENROLL, - disableSnooze: boolean = false, - ): Promise { + private async startEnrollment(enrollmentType: ModalType.CERTIFICATE_RENEWAL | ModalType.ENROLL): Promise { // If the user has already started enrolment, don't show the notification. Instead, show the loading modal // This will occur after the redirect from the oauth provider if (this.coreE2EIService.isEnrollmentInProgress()) { return this.enroll(); } - // Early return if we shouldn't show the notification - if (!this.shouldShowNotification()) { + if (this.config?.timer.isSnoozableTimerActive()) { + // If the user has snoozed, no need to show the notification modal return; } - this.initializeEnrollmentTimer(); - // If the timer is not active, show the notification modal - if (this.config && !this.config.timer.isDelayTimerActive()) { - return this.showEnrollmentModal(modalType); + if (this.config) { + return this.showEnrollmentModal(enrollmentType, this.config); } } } diff --git a/src/script/E2EIdentity/DelayTimer/DelayTimer.test.ts b/src/script/E2EIdentity/SnoozableTimer/SnoozableTimer.test.ts similarity index 79% rename from src/script/E2EIdentity/DelayTimer/DelayTimer.test.ts rename to src/script/E2EIdentity/SnoozableTimer/SnoozableTimer.test.ts index 2a7a3b7087f..d12e0d637d2 100644 --- a/src/script/E2EIdentity/DelayTimer/DelayTimer.test.ts +++ b/src/script/E2EIdentity/SnoozableTimer/SnoozableTimer.test.ts @@ -18,19 +18,19 @@ */ import {FIFTEEN_MINUTES, FOUR_HOURS, ONE_HOUR, ONE_MINUTE} from './delay'; -import {DelayTimerService} from './DelayTimer'; +import {SnoozableTimer} from './SnoozableTimer'; describe('createGracePeriodTimer', () => { - let timer: DelayTimerService | undefined; + let timer: SnoozableTimer | undefined; beforeEach(() => { jest.clearAllMocks(); jest.useFakeTimers(); global.localStorage.clear(); - timer = new DelayTimerService({ + timer = new SnoozableTimer({ gracePeriodInMS: 0, - gracePeriodExpiredCallback: jest.fn(), - delayPeriodExpiredCallback: jest.fn(), + onGracePeriodExpired: jest.fn(), + onSnoozeExpired: jest.fn(), }); }); @@ -42,8 +42,8 @@ describe('createGracePeriodTimer', () => { const gracePeriodExpiredCallback = jest.fn(); timer?.updateParams({ gracePeriodInMS: 1000, - gracePeriodExpiredCallback, - delayPeriodExpiredCallback: jest.fn(), + onGracePeriodExpired: gracePeriodExpiredCallback, + onSnoozeExpired: jest.fn(), }); jest.advanceTimersByTime(1000); @@ -55,11 +55,11 @@ describe('createGracePeriodTimer', () => { timer?.updateParams({ gracePeriodInMS: ONE_HOUR, - gracePeriodExpiredCallback, - delayPeriodExpiredCallback: jest.fn(), + onGracePeriodExpired: gracePeriodExpiredCallback, + onSnoozeExpired: jest.fn(), }); - timer?.delayPrompt(); + timer?.snooze(); jest.advanceTimersByTime(FIFTEEN_MINUTES); expect(gracePeriodExpiredCallback).not.toHaveBeenCalled(); @@ -72,10 +72,10 @@ describe('createGracePeriodTimer', () => { const gracePeriodExpiredCallback = jest.fn(); timer?.updateParams({ gracePeriodInMS: 0, - gracePeriodExpiredCallback, - delayPeriodExpiredCallback: jest.fn(), + onGracePeriodExpired: gracePeriodExpiredCallback, + onSnoozeExpired: jest.fn(), }); - timer?.delayPrompt(); + timer?.snooze(); jest.advanceTimersByTime(500); expect(gracePeriodExpiredCallback).toHaveBeenCalled(); @@ -85,12 +85,12 @@ describe('createGracePeriodTimer', () => { const gracePeriodExpiredCallback = jest.fn(); timer?.updateParams({ gracePeriodInMS: 7200000, - gracePeriodExpiredCallback, - delayPeriodExpiredCallback: jest.fn(), + onGracePeriodExpired: gracePeriodExpiredCallback, + onSnoozeExpired: jest.fn(), }); - timer?.delayPrompt(); + timer?.snooze(); jest.advanceTimersByTime(3600000); - timer?.delayPrompt(); + timer?.snooze(); jest.advanceTimersByTime(3600000); expect(gracePeriodExpiredCallback).toHaveBeenCalled(); @@ -100,11 +100,11 @@ describe('createGracePeriodTimer', () => { const delayPeriodExpiredCallback = jest.fn(); timer?.updateParams({ gracePeriodInMS: ONE_HOUR, - gracePeriodExpiredCallback: jest.fn(), - delayPeriodExpiredCallback, + onGracePeriodExpired: jest.fn(), + onSnoozeExpired: delayPeriodExpiredCallback, }); - timer?.delayPrompt(); + timer?.snooze(); // getDelayTime(ONE_HOUR) will return FIFTEEN_MINUTES according to the function provided. jest.advanceTimersByTime(FIFTEEN_MINUTES); @@ -116,18 +116,18 @@ describe('createGracePeriodTimer', () => { const gracePeriodExpiredCallback = jest.fn(); timer?.updateParams({ gracePeriodInMS: ONE_HOUR, - delayPeriodExpiredCallback, - gracePeriodExpiredCallback, + onSnoozeExpired: delayPeriodExpiredCallback, + onGracePeriodExpired: gracePeriodExpiredCallback, }); - timer?.delayPrompt(); + timer?.snooze(); // Here, instead of advancing time by "ONE_HOUR + FIFTEEN_MINUTES", we advance by "ONE_HOUR", which is the end of the grace period. jest.advanceTimersByTime(ONE_HOUR + FIFTEEN_MINUTES); expect(delayPeriodExpiredCallback).toHaveBeenCalled(); // The delayPeriodExpiredCallback should be called after ONE_HOUR. expect(gracePeriodExpiredCallback).toHaveBeenCalled(); // The gracePeriodExpiredCallback should be called when the grace period ends, which is after ONE_HOUR. - timer?.delayPrompt(); // We try to delay after the grace period has ended. + timer?.snooze(); // We try to delay after the grace period has ended. jest.advanceTimersByTime(FIFTEEN_MINUTES); expect(delayPeriodExpiredCallback).toHaveBeenCalledTimes(1); // The delayPeriodExpiredCallback should not be called again since we're now past the grace period. }); @@ -136,17 +136,17 @@ describe('createGracePeriodTimer', () => { const delayPeriodExpiredCallback = jest.fn(); timer?.updateParams({ gracePeriodInMS: FOUR_HOURS, - gracePeriodExpiredCallback: jest.fn(), - delayPeriodExpiredCallback, + onGracePeriodExpired: jest.fn(), + onSnoozeExpired: delayPeriodExpiredCallback, }); - timer?.delayPrompt(); + timer?.snooze(); jest.advanceTimersByTime(ONE_HOUR); // gracePeriod > delay, so delay = ONE_HOUR - timer?.delayPrompt(); + timer?.snooze(); jest.advanceTimersByTime(ONE_HOUR); - timer?.delayPrompt(); + timer?.snooze(); jest.advanceTimersByTime(ONE_HOUR); expect(delayPeriodExpiredCallback).toHaveBeenCalledTimes(3); @@ -157,11 +157,11 @@ describe('createGracePeriodTimer', () => { const gracePeriodExpiredCallback = jest.fn(); timer?.updateParams({ gracePeriodInMS: ONE_MINUTE, - gracePeriodExpiredCallback, - delayPeriodExpiredCallback, + onGracePeriodExpired: gracePeriodExpiredCallback, + onSnoozeExpired: delayPeriodExpiredCallback, }); - timer?.delayPrompt(); + timer?.snooze(); jest.advanceTimersByTime(ONE_MINUTE); expect(delayPeriodExpiredCallback).not.toHaveBeenCalled(); diff --git a/src/script/E2EIdentity/DelayTimer/DelayTimer.ts b/src/script/E2EIdentity/SnoozableTimer/SnoozableTimer.ts similarity index 67% rename from src/script/E2EIdentity/DelayTimer/DelayTimer.ts rename to src/script/E2EIdentity/SnoozableTimer/SnoozableTimer.ts index 3bba5a2b7f6..4147eb543cb 100644 --- a/src/script/E2EIdentity/DelayTimer/DelayTimer.ts +++ b/src/script/E2EIdentity/SnoozableTimer/SnoozableTimer.ts @@ -21,46 +21,53 @@ import logdown from 'logdown'; import {util} from '@wireapp/core'; -import {getDelayTime} from './delay'; -import {DelayTimerStore} from './DelayTimerStorage'; +import {getSnoozeTime} from './delay'; +import {SnoozableTimerStore} from './SnoozableTimerStorage'; const {TaskScheduler} = util; interface CreateGracePeriodTimerParams { gracePeriodInMS: number; - gracePeriodExpiredCallback: () => void; - delayPeriodExpiredCallback: () => void; + /** + * called when the grace period is over. The grace period is the time during which the user can keep snoozing the enrollment. + * Once the grace period is over, the user will be forced to enroll. + */ + onGracePeriodExpired: () => void; + /** + * as long as the grace period is not over, the user can snooze the enrollment. This callback is called when the user snoozes the enrollment. + */ + onSnoozeExpired: () => void; } -export class DelayTimerService { +export class SnoozableTimer { private gracePeriodInMS: number; - private gracePeriodExpiredCallback: () => void; - private delayPeriodExpiredCallback: () => void; + private onGracePeriodExpired: () => void; + private onSnoozeExpired: () => void; private readonly logger = logdown('@wireapp/core/DelayTimer'); private delayPeriodTimerKey: string = 'E2EIdentity_DelayTimer'; private gracePeriodTimerKey: string = 'E2EIdentity_GracePeriodTimer'; - constructor({gracePeriodInMS, gracePeriodExpiredCallback, delayPeriodExpiredCallback}: CreateGracePeriodTimerParams) { + constructor({ + gracePeriodInMS, + onGracePeriodExpired: onGracePeriodExpired, + onSnoozeExpired, + }: CreateGracePeriodTimerParams) { this.gracePeriodInMS = gracePeriodInMS; - this.gracePeriodExpiredCallback = gracePeriodExpiredCallback; - this.delayPeriodExpiredCallback = delayPeriodExpiredCallback; + this.onGracePeriodExpired = onGracePeriodExpired; + this.onSnoozeExpired = onSnoozeExpired; this.initialize(); } /** * @param CreateGracePeriodTimerParams The params to create the grace period timer */ - public updateParams({ - gracePeriodInMS, - gracePeriodExpiredCallback, - delayPeriodExpiredCallback, - }: CreateGracePeriodTimerParams) { - DelayTimerStore.clear.all(); + public updateParams({gracePeriodInMS, onGracePeriodExpired, onSnoozeExpired}: CreateGracePeriodTimerParams) { + SnoozableTimerStore.clear.all(); this.clearGracePeriodTimer(); - this.clearDelayPeriodTimer(); + this.clearSnoozePeriodTimer(); this.gracePeriodInMS = gracePeriodInMS; - this.gracePeriodExpiredCallback = gracePeriodExpiredCallback; - this.delayPeriodExpiredCallback = delayPeriodExpiredCallback; + this.onGracePeriodExpired = onGracePeriodExpired; + this.onSnoozeExpired = onSnoozeExpired; this.initialize(); } @@ -73,7 +80,7 @@ export class DelayTimerService { } // Check if grace period has changed - if (DelayTimerStore.get.gracePeriod() !== this.gracePeriodInMS) { + if (SnoozableTimerStore.get.gracePeriod() !== this.gracePeriodInMS) { // Check if grace period is less than the time elapsed since the last prompt if (this.gracePeriodInMS < this.getElapsedGracePeriod()) { return this.exit( @@ -84,44 +91,44 @@ export class DelayTimerService { } // Load saved data from local storage - if (DelayTimerStore.get.firingDate()) { + if (SnoozableTimerStore.get.firingDate()) { const currentTime = Date.now(); - if (DelayTimerStore.get.firingDate() <= currentTime) { + if (SnoozableTimerStore.get.firingDate() <= currentTime) { return this.exit('Grace period is already over. No more delays are allowed.'); } } else { const firingDate = Date.now() + this.gracePeriodInMS; - DelayTimerStore.store.firingDate(firingDate); - DelayTimerStore.store.gracePeriod(this.gracePeriodInMS); + SnoozableTimerStore.store.firingDate(firingDate); + SnoozableTimerStore.store.gracePeriod(this.gracePeriodInMS); } // Start / restart the grace period timer - this.startGracePeriod(DelayTimerStore.get.firingDate()); + this.startGracePeriod(SnoozableTimerStore.get.firingDate()); // this will start the delay period timer if it was active before - this.continueDelayPeriodTimer(); + this.continueSnoozePeriodTimer(); } /** - * Prompt the user to delay the enrollment + * Will start a snooze period if the conditions are met */ - public delayPrompt() { - if (this.isDelayTimerActive()) { + public snooze() { + if (this.isSnoozableTimerActive()) { return; } if (!this.isSnoozeTimeAvailable()) { return this.exit('No more delays are allowed.'); } - const delayTimeInMS = getDelayTime(this.gracePeriodInMS); + const delayTimeInMS = getSnoozeTime(this.gracePeriodInMS); if (delayTimeInMS <= 0) { return this.exit('Delay period is 0. No more delays are allowed.'); } - if (DelayTimerStore.get.firingDate() <= Date.now()) { + if (SnoozableTimerStore.get.firingDate() <= Date.now()) { return this.exit('Grace period is already over. No more delays are allowed.'); } - this.startDelayPeriod(Date.now() + delayTimeInMS); + this.startSnoozePeriod(Date.now() + delayTimeInMS); } /** @@ -129,7 +136,7 @@ export class DelayTimerService { */ private updateGracePeriod() { // Store the new grace period - DelayTimerStore.store.gracePeriod(this.gracePeriodInMS); + SnoozableTimerStore.store.gracePeriod(this.gracePeriodInMS); const elapsedGracePeriod = this.getElapsedGracePeriod(); // Check if grace period is already over @@ -143,7 +150,7 @@ export class DelayTimerService { // Calculate the new end time const firingDate = startTime + this.gracePeriodInMS; // Store the new end time - DelayTimerStore.store.firingDate(firingDate); + SnoozableTimerStore.store.firingDate(firingDate); this.startGracePeriod(firingDate); } @@ -155,10 +162,10 @@ export class DelayTimerService { */ private exit(exitMessage: string) { this.logger.info(exitMessage); - this.clearDelayPeriodTimer(); + this.clearSnoozePeriodTimer(); this.clearGracePeriodTimer(); - DelayTimerStore.clear.all(); - return this.gracePeriodExpiredCallback(); + SnoozableTimerStore.clear.all(); + return this.onGracePeriodExpired(); } /** @@ -191,12 +198,12 @@ export class DelayTimerService { * Start the delay period timer and store the delay time * @param delayTimeInMS The delay time in ms */ - private startDelayPeriod(firingDate?: number) { - this.clearDelayPeriodTimer(); + private startSnoozePeriod(firingDate?: number) { + this.clearSnoozePeriodTimer(); const task = () => { - this.logger.info('Delay time is over.'); - return this.delayPeriodExpiredCallback(); + this.logger.info('Snooze time is over.'); + return this.onSnoozeExpired(); }; if (TaskScheduler.hasActiveTask(this.delayPeriodTimerKey)) { @@ -224,12 +231,12 @@ export class DelayTimerService { /** * Clear the current delay period timer */ - private clearDelayPeriodTimer() { + private clearSnoozePeriodTimer() { TaskScheduler.cancelTask(this.delayPeriodTimerKey); } - private continueDelayPeriodTimer() { - this.startDelayPeriod(); + private continueSnoozePeriodTimer() { + this.startSnoozePeriod(); } /** @@ -237,18 +244,18 @@ export class DelayTimerService { * @returns The time elapsed since the last prompt in ms */ private getElapsedGracePeriod() { - return DelayTimerStore.get.firingDate() - ? Date.now() - (DelayTimerStore.get.firingDate() - this.gracePeriodInMS) + return SnoozableTimerStore.get.firingDate() + ? Date.now() - (SnoozableTimerStore.get.firingDate() - this.gracePeriodInMS) : 0; } - public isDelayTimerActive() { + public isSnoozableTimerActive() { return TaskScheduler.hasActiveTask(this.delayPeriodTimerKey); } public isSnoozeTimeAvailable() { - const remainingTime = DelayTimerStore.get.firingDate() - Date.now(); - const delayTime = getDelayTime(remainingTime); + const remainingTime = SnoozableTimerStore.get.firingDate() - Date.now(); + const delayTime = getSnoozeTime(remainingTime); return remainingTime - delayTime > 0; } } diff --git a/src/script/E2EIdentity/DelayTimer/DelayTimerStorage.ts b/src/script/E2EIdentity/SnoozableTimer/SnoozableTimerStorage.ts similarity index 90% rename from src/script/E2EIdentity/DelayTimer/DelayTimerStorage.ts rename to src/script/E2EIdentity/SnoozableTimer/SnoozableTimerStorage.ts index 7c122af9975..d30f1da11b8 100644 --- a/src/script/E2EIdentity/DelayTimer/DelayTimerStorage.ts +++ b/src/script/E2EIdentity/SnoozableTimer/SnoozableTimerStorage.ts @@ -20,7 +20,7 @@ const FiringDateKey = 'E2EIdentity_DelayTimer_FiringDate'; const GracePeriodKey = 'E2EIdentity_DelayTimer_GracePeriod'; -const DelayTimerStore = { +export const SnoozableTimerStore = { store: { firingDate: (firingDate: number) => localStorage.setItem(FiringDateKey, String(firingDate)), gracePeriod: (gracePeriod: number) => localStorage.setItem(GracePeriodKey, String(gracePeriod)), @@ -33,10 +33,8 @@ const DelayTimerStore = { firingDate: () => localStorage.removeItem(FiringDateKey), gracePeriod: () => localStorage.removeItem(GracePeriodKey), all: () => { - DelayTimerStore.clear.firingDate(); - DelayTimerStore.clear.gracePeriod(); + SnoozableTimerStore.clear.firingDate(); + SnoozableTimerStore.clear.gracePeriod(); }, }, }; - -export {DelayTimerStore}; diff --git a/src/script/E2EIdentity/DelayTimer/delay.ts b/src/script/E2EIdentity/SnoozableTimer/delay.ts similarity index 91% rename from src/script/E2EIdentity/DelayTimer/delay.ts rename to src/script/E2EIdentity/SnoozableTimer/delay.ts index 951de7add5a..b6ecafaed8f 100644 --- a/src/script/E2EIdentity/DelayTimer/delay.ts +++ b/src/script/E2EIdentity/SnoozableTimer/delay.ts @@ -38,7 +38,11 @@ export const ONE_HOUR = TIME_IN_MILLIS.HOUR; export const FOUR_HOURS = TIME_IN_MILLIS.HOUR * 4; export const ONE_DAY = TIME_IN_MILLIS.DAY; -export function getDelayTime(gracePeriodInMs: number): number { +/** + * Will return a suitable snooze time based on the grace period + * @param gracePeriodInMs - the full grace period length in milliseconds + */ +export function getSnoozeTime(gracePeriodInMs: number): number { if (gracePeriodInMs > 0) { if (gracePeriodInMs <= FIFTEEN_MINUTES) { return Math.min(FIVE_MINUTES, gracePeriodInMs); diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts index af270b65a6e..01a66f16893 100644 --- a/src/script/conversation/ConversationRepository.ts +++ b/src/script/conversation/ConversationRepository.ts @@ -2255,7 +2255,7 @@ export class ConversationRepository { */ private async clearConversationContent(conversation: Conversation, timestamp: number) { await this.deleteMessages(conversation, timestamp); - await this.addCreationMessage(conversation, !!this.userState.self()?.isTemporaryGuest(), timestamp); + await this.addCreationMessage(conversation, !!this.userState.self()?.isTemporaryGuest()); conversation.setTimestamp(timestamp, Conversation.TIMESTAMP_TYPE.CLEARED); } diff --git a/src/script/conversation/EventBuilder.ts b/src/script/conversation/EventBuilder.ts index b122cb8982f..b6d80ede2c8 100644 --- a/src/script/conversation/EventBuilder.ts +++ b/src/script/conversation/EventBuilder.ts @@ -430,11 +430,11 @@ export const EventBuilder = { buildGroupCreation( conversationEntity: Conversation, isTemporaryGuest: boolean = false, - timestamp: number, + timestamp: number = 0, ): GroupCreationEvent { const {creator: creatorId} = conversationEntity; const selfUserId = conversationEntity.selfUser().id; - const isoDate = new Date(timestamp || 0).toISOString(); + const isoDate = new Date(timestamp).toISOString(); const userIds = conversationEntity.participating_user_ids().slice(); const createdBySelf = creatorId === selfUserId || isTemporaryGuest; diff --git a/src/script/error/ConversationError.ts b/src/script/error/ConversationError.ts index 92495ed8a4e..a14daf5b212 100644 --- a/src/script/error/ConversationError.ts +++ b/src/script/error/ConversationError.ts @@ -21,6 +21,7 @@ import {BaseError, BASE_ERROR_TYPE} from './BaseError'; enum CONVERSATION_ERROR_TYPE { CONVERSATION_NOT_FOUND = 'CONVERSATION_NOT_FOUND', + CONVERSATION_WITH_BLOCKED_USER = 'CONVERSATION_WITH_BLOCKED_USER', INVALID_PARAMETER = 'INVALID_PARAMETER', LEGAL_HOLD_CONVERSATION_CANCELLATION = 'LEGAL_HOLD_CONVERSATION_CANCELLATION', MESSAGE_NOT_FOUND = 'MESSAGE_NOT_FOUND', @@ -46,6 +47,7 @@ export class ConversationError extends BaseError { static get MESSAGE(): Record { return { CONVERSATION_NOT_FOUND: 'Conversation not found', + CONVERSATION_WITH_BLOCKED_USER: 'Conversation not reachable', INVALID_PARAMETER: 'Invalid parameter passed', LEGAL_HOLD_CONVERSATION_CANCELLATION: 'Sending to legal hold conversation was canceled by user', MESSAGE_NOT_FOUND: 'Message not found in conversation', @@ -64,6 +66,7 @@ export class ConversationError extends BaseError { static get TYPE(): Record { return { CONVERSATION_NOT_FOUND: CONVERSATION_ERROR_TYPE.CONVERSATION_NOT_FOUND, + CONVERSATION_WITH_BLOCKED_USER: CONVERSATION_ERROR_TYPE.CONVERSATION_WITH_BLOCKED_USER, INVALID_PARAMETER: CONVERSATION_ERROR_TYPE.INVALID_PARAMETER, LEGAL_HOLD_CONVERSATION_CANCELLATION: CONVERSATION_ERROR_TYPE.LEGAL_HOLD_CONVERSATION_CANCELLATION, MESSAGE_NOT_FOUND: CONVERSATION_ERROR_TYPE.MESSAGE_NOT_FOUND, diff --git a/src/script/hooks/useAppSoftLock.ts b/src/script/hooks/useAppSoftLock.ts index 797f74b8733..89e9bfcd95a 100644 --- a/src/script/hooks/useAppSoftLock.ts +++ b/src/script/hooks/useAppSoftLock.ts @@ -21,7 +21,7 @@ import {useCallback, useEffect, useState} from 'react'; import {CallingRepository} from '../calling/CallingRepository'; import {E2EIHandler, EnrollmentConfig, isE2EIEnabled, WireIdentity} from '../E2EIdentity'; -import {shouldEnableSoftLock} from '../E2EIdentity/DelayTimer/delay'; +import {shouldEnableSoftLock} from '../E2EIdentity/SnoozableTimer/delay'; import {NotificationRepository} from '../notification/NotificationRepository'; export function useAppSoftLock(callingRepository: CallingRepository, notificationRepository: NotificationRepository) { diff --git a/src/script/view_model/ContentViewModel.ts b/src/script/view_model/ContentViewModel.ts index 876bebbbf80..0a9da2b5fa7 100644 --- a/src/script/view_model/ContentViewModel.ts +++ b/src/script/view_model/ContentViewModel.ts @@ -219,10 +219,27 @@ export class ContentViewModel { ); } + private showConversationWithBlockedUserErrorModal(): void { + PrimaryModal.show( + PrimaryModal.type.ACKNOWLEDGE, + { + text: { + message: t('conversationWithBlockedUserMessage'), + title: t('conversationWithBlockedUserTitle'), + }, + }, + undefined, + ); + } + private isConversationNotFoundError(error: any): boolean { return error.type === ConversationError.TYPE.CONVERSATION_NOT_FOUND; } + private isConversationWithBlockedUserError(error: any): boolean { + return error.type === ConversationError.TYPE.CONVERSATION_WITH_BLOCKED_USER; + } + /** * Opens the specified conversation. * @@ -249,6 +266,7 @@ export class ContentViewModel { try { const conversationEntity = await this.getConversationEntity(conversation, domain); + const isConnectionBlocked = conversationEntity?.connection()?.isBlocked(); if (!conversationEntity) { this.closeRightSidebar(); @@ -258,6 +276,14 @@ export class ContentViewModel { ); } + if (isConnectionBlocked) { + this.closeRightSidebar(); + throw new ConversationError( + ConversationError.TYPE.CONVERSATION_WITH_BLOCKED_USER, + ConversationError.MESSAGE.CONVERSATION_WITH_BLOCKED_USER, + ); + } + const isActiveConversation = this.conversationState.isActiveConversation(conversationEntity); if (!isActiveConversation) { @@ -276,10 +302,14 @@ export class ContentViewModel { this.showAndNavigate(conversationEntity, openNotificationSettings); } catch (error: any) { if (this.isConversationNotFoundError(error)) { - this.showConversationNotFoundErrorModal(); - } else { - throw error; + return this.showConversationNotFoundErrorModal(); } + + if (this.isConversationWithBlockedUserError(error)) { + return this.showConversationWithBlockedUserErrorModal(); + } + + throw error; } };