From be3b904f621ffd1b0c7bfb5252027c15326a043f Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 1 Sep 2023 22:31:35 +0530 Subject: [PATCH] chore: show invite count bubble on preferences button (#2458) --- .../src/Domain}/Preferences/PreferenceId.ts | 4 +-- .../src/Domain/Status/StatusService.ts | 34 +++++++++++++++++++ .../Domain/Status/StatusServiceInterface.ts | 6 ++++ .../Domain/VaultInvite/VaultInviteService.ts | 33 ++++++++++++++---- packages/services/src/Domain/index.ts | 1 + .../Application/Dependencies/Dependencies.ts | 1 + .../Dependencies/DependencyEvents.ts | 1 + .../src/Route/Params/SettingsParams.ts | 4 +-- packages/ui-services/src/Route/RouteParser.ts | 4 +-- packages/ui-services/src/index.ts | 1 - .../javascripts/Application/WebApplication.ts | 4 +-- .../javascripts/Components/Footer/Footer.tsx | 7 ++-- .../Components/Footer/PreferencesButton.tsx | 23 ++++++++++--- .../Controller/PreferencesMenuItem.ts | 7 ++-- .../PreferencesSessionController.ts | 13 +++---- .../Vaults/Invites/ContactInviteModal.tsx | 12 ++++--- .../Preferences/Panes/Vaults/Vaults.tsx | 14 ++++---- .../PreferencesComponents/MenuItem.tsx | 25 +++++++++++--- .../Preferences/PreferencesMenuView.tsx | 7 ++-- .../Controllers/PreferencesController.ts | 11 +++--- 20 files changed, 158 insertions(+), 54 deletions(-) rename packages/{ui-services/src => services/src/Domain}/Preferences/PreferenceId.ts (68%) diff --git a/packages/ui-services/src/Preferences/PreferenceId.ts b/packages/services/src/Domain/Preferences/PreferenceId.ts similarity index 68% rename from packages/ui-services/src/Preferences/PreferenceId.ts rename to packages/services/src/Domain/Preferences/PreferenceId.ts index 17a38e0dc..a501354d0 100644 --- a/packages/ui-services/src/Preferences/PreferenceId.ts +++ b/packages/services/src/Domain/Preferences/PreferenceId.ts @@ -1,4 +1,4 @@ -const PREFERENCE_IDS = [ +const PREFERENCE_PANE_IDS = [ 'general', 'account', 'security', @@ -14,4 +14,4 @@ const PREFERENCE_IDS = [ 'whats-new', ] as const -export type PreferenceId = (typeof PREFERENCE_IDS)[number] +export type PreferencePaneId = (typeof PREFERENCE_PANE_IDS)[number] diff --git a/packages/services/src/Domain/Status/StatusService.ts b/packages/services/src/Domain/Status/StatusService.ts index 077636251..a8a822450 100644 --- a/packages/services/src/Domain/Status/StatusService.ts +++ b/packages/services/src/Domain/Status/StatusService.ts @@ -1,10 +1,44 @@ import { removeFromArray } from '@standardnotes/utils' import { AbstractService } from '../Service/AbstractService' import { StatusServiceEvent, StatusServiceInterface, StatusMessageIdentifier } from './StatusServiceInterface' +import { PreferencePaneId } from '../Preferences/PreferenceId' /* istanbul ignore file */ export class StatusService extends AbstractService implements StatusServiceInterface { + private preferencesBubbleCounts: Record = { + general: 0, + account: 0, + security: 0, + 'home-server': 0, + vaults: 0, + appearance: 0, + backups: 0, + listed: 0, + shortcuts: 0, + accessibility: 0, + 'get-free-month': 0, + 'help-feedback': 0, + 'whats-new': 0, + } + + getPreferencesBubbleCount(preferencePaneId: PreferencePaneId): number { + return this.preferencesBubbleCounts[preferencePaneId] + } + + setPreferencesBubbleCount(preferencePaneId: PreferencePaneId, count: number): void { + this.preferencesBubbleCounts[preferencePaneId] = count + const totalCount = this.totalPreferencesBubbleCount + void this.notifyEvent( + StatusServiceEvent.PreferencesBubbleCountChanged, + totalCount > 0 ? totalCount.toString() : undefined, + ) + } + + get totalPreferencesBubbleCount(): number { + return Object.values(this.preferencesBubbleCounts).reduce((total, count) => total + count, 0) + } + private _message = '' private directSetMessage?: string private dynamicMessages: string[] = [] diff --git a/packages/services/src/Domain/Status/StatusServiceInterface.ts b/packages/services/src/Domain/Status/StatusServiceInterface.ts index dc347f717..5ccd06c60 100644 --- a/packages/services/src/Domain/Status/StatusServiceInterface.ts +++ b/packages/services/src/Domain/Status/StatusServiceInterface.ts @@ -1,14 +1,20 @@ +import { PreferencePaneId } from '../Preferences/PreferenceId' import { AbstractService } from '../Service/AbstractService' /* istanbul ignore file */ export enum StatusServiceEvent { MessageChanged = 'MessageChanged', + PreferencesBubbleCountChanged = 'PreferencesBubbleCountChanged', } export type StatusMessageIdentifier = string export interface StatusServiceInterface extends AbstractService { + getPreferencesBubbleCount(preferencePaneId: PreferencePaneId): number + setPreferencesBubbleCount(preferencePaneId: PreferencePaneId, count: number): void + get totalPreferencesBubbleCount(): number + get message(): string setMessage(message: string | undefined): void addMessage(message: string): StatusMessageIdentifier diff --git a/packages/services/src/Domain/VaultInvite/VaultInviteService.ts b/packages/services/src/Domain/VaultInvite/VaultInviteService.ts index 1fa70a91b..af10bc98c 100644 --- a/packages/services/src/Domain/VaultInvite/VaultInviteService.ts +++ b/packages/services/src/Domain/VaultInvite/VaultInviteService.ts @@ -37,6 +37,8 @@ import { AbstractService } from './../Service/AbstractService' import { VaultInviteServiceEvent } from './VaultInviteServiceEvent' import { GetKeyPairs } from '../Encryption/UseCase/GetKeyPairs' import { DecryptErroredPayloads } from '../Encryption/UseCase/DecryptErroredPayloads' +import { StatusServiceInterface } from '../Status/StatusServiceInterface' +import { ApplicationEvent } from '../Event/ApplicationEvent' import { WebSocketsServiceEvent } from '../Api/WebSocketsServiceEvent' export class VaultInviteService @@ -51,6 +53,7 @@ export class VaultInviteService private vaultUsers: VaultUserServiceInterface, private sync: SyncServiceInterface, private invitesServer: SharedVaultInvitesServer, + private status: StatusServiceInterface, private _getAllContacts: GetAllContacts, private _getVault: GetVault, private _getVaultContacts: GetVaultContacts, @@ -96,11 +99,28 @@ export class VaultInviteService this.pendingInvites = {} } + updatePendingInviteCount() { + this.status.setPreferencesBubbleCount('vaults', Object.keys(this.pendingInvites).length) + } + + addPendingInvite(invite: InviteRecord): void { + this.pendingInvites[invite.invite.uuid] = invite + this.updatePendingInviteCount() + } + + removePendingInvite(uuid: string): void { + delete this.pendingInvites[uuid] + this.updatePendingInviteCount() + } + async handleEvent(event: InternalEventInterface): Promise { switch (event.type) { case SyncEvent.ReceivedSharedVaultInvites: await this.processInboundInvites(event.payload as SyncEventReceivedSharedVaultInvitesData) break + case ApplicationEvent.Launched: + void this.downloadInboundInvites() + break case WebSocketsServiceEvent.UserInvitedToSharedVault: await this.processInboundInvites([(event as UserInvitedToSharedVaultEvent).payload.invite]) break @@ -154,7 +174,7 @@ export class VaultInviteService return Result.fail(acceptResult.getError()) } - delete this.pendingInvites[pendingInvite.invite.uuid] + this.removePendingInvite(pendingInvite.invite.uuid) void this.sync.sync() @@ -242,7 +262,7 @@ export class VaultInviteService return ClientDisplayableError.FromString(`Failed to delete invite ${JSON.stringify(response)}`) } - delete this.pendingInvites[invite.uuid] + this.removePendingInvite(invite.uuid) } private async reprocessCachedInvitesTrustStatusAfterTrustedContactsChange(): Promise { @@ -253,6 +273,7 @@ export class VaultInviteService private async processInboundInvites(invites: SharedVaultInviteServerHash[]): Promise { if (invites.length === 0) { + this.updatePendingInviteCount() return } @@ -274,11 +295,11 @@ export class VaultInviteService }) if (!trustedMessage.isFailed()) { - this.pendingInvites[invite.uuid] = { + this.addPendingInvite({ invite, message: trustedMessage.getValue(), trusted: true, - } + }) continue } @@ -290,11 +311,11 @@ export class VaultInviteService }) if (!untrustedMessage.isFailed()) { - this.pendingInvites[invite.uuid] = { + this.addPendingInvite({ invite, message: untrustedMessage.getValue(), trusted: false, - } + }) } } diff --git a/packages/services/src/Domain/index.ts b/packages/services/src/Domain/index.ts index 35d65d138..d738d6195 100644 --- a/packages/services/src/Domain/index.ts +++ b/packages/services/src/Domain/index.ts @@ -134,6 +134,7 @@ export * from './KeySystem/KeySystemKeyManager' export * from './Mfa/MfaServiceInterface' export * from './Mutator/MutatorClientInterface' export * from './Payloads/PayloadManagerInterface' +export * from './Preferences/PreferenceId' export * from './Preferences/PreferenceServiceInterface' export * from './Protection/MobileUnlockTiming' export * from './Protection/ProtectionClientInterface' diff --git a/packages/snjs/lib/Application/Dependencies/Dependencies.ts b/packages/snjs/lib/Application/Dependencies/Dependencies.ts index aa48b40c5..1de484c48 100644 --- a/packages/snjs/lib/Application/Dependencies/Dependencies.ts +++ b/packages/snjs/lib/Application/Dependencies/Dependencies.ts @@ -864,6 +864,7 @@ export class Dependencies { this.get(TYPES.VaultUserService), this.get(TYPES.SyncService), this.get(TYPES.SharedVaultInvitesServer), + this.get(TYPES.StatusService), this.get(TYPES.GetAllContacts), this.get(TYPES.GetVault), this.get(TYPES.GetVaultContacts), diff --git a/packages/snjs/lib/Application/Dependencies/DependencyEvents.ts b/packages/snjs/lib/Application/Dependencies/DependencyEvents.ts index 93181dc1b..0cdd89547 100644 --- a/packages/snjs/lib/Application/Dependencies/DependencyEvents.ts +++ b/packages/snjs/lib/Application/Dependencies/DependencyEvents.ts @@ -37,6 +37,7 @@ export function RegisterApplicationServicesEvents(container: Dependencies, event events.addEventHandler(container.get(TYPES.SubscriptionManager), SessionEvent.Restored) events.addEventHandler(container.get(TYPES.SyncService), IntegrityEvent.IntegrityCheckCompleted) events.addEventHandler(container.get(TYPES.UserService), AccountEvent.SignedInOrRegistered) + events.addEventHandler(container.get(TYPES.VaultInviteService), ApplicationEvent.Launched) events.addEventHandler(container.get(TYPES.VaultInviteService), SyncEvent.ReceivedSharedVaultInvites) if (container.get(TYPES.FilesBackupService)) { diff --git a/packages/ui-services/src/Route/Params/SettingsParams.ts b/packages/ui-services/src/Route/Params/SettingsParams.ts index c77a4de7b..00541d731 100644 --- a/packages/ui-services/src/Route/Params/SettingsParams.ts +++ b/packages/ui-services/src/Route/Params/SettingsParams.ts @@ -1,5 +1,5 @@ -import { PreferenceId } from '../../Preferences/PreferenceId' +import { PreferencePaneId } from '@standardnotes/services' export type SettingsParams = { - panel: PreferenceId + panel: PreferencePaneId } diff --git a/packages/ui-services/src/Route/RouteParser.ts b/packages/ui-services/src/Route/RouteParser.ts index d9b1e3fc1..0574cf0de 100644 --- a/packages/ui-services/src/Route/RouteParser.ts +++ b/packages/ui-services/src/Route/RouteParser.ts @@ -1,5 +1,5 @@ import { UserRequestType } from '@standardnotes/common' -import { PreferenceId } from './../Preferences/PreferenceId' +import { PreferencePaneId } from '@standardnotes/services' import { AppViewRouteParam, ValidAppViewRoutes } from './Params/AppViewRouteParams' import { DemoParams } from './Params/DemoParams' import { OnboardingParams } from './Params/OnboardingParams' @@ -58,7 +58,7 @@ export class RouteParser implements RouteParserInterface { this.checkForProperRouteType(RouteType.Settings) return { - panel: this.searchParams.get(RootQueryParam.Settings) as PreferenceId, + panel: this.searchParams.get(RootQueryParam.Settings) as PreferencePaneId, } } diff --git a/packages/ui-services/src/index.ts b/packages/ui-services/src/index.ts index f6de720b5..0f69bfcf5 100644 --- a/packages/ui-services/src/index.ts +++ b/packages/ui-services/src/index.ts @@ -12,7 +12,6 @@ export * from './Keyboard/KeyboardKey' export * from './Keyboard/KeyboardModifier' export * from './Keyboard/keyboardCharacterForModifier' export * from './Keyboard/keyboardStringForShortcut' -export * from './Preferences/PreferenceId' export * from './Route/Params/DemoParams' export * from './Route/Params/OnboardingParams' export * from './Route/Params/PurchaseParams' diff --git a/packages/web/src/javascripts/Application/WebApplication.ts b/packages/web/src/javascripts/Application/WebApplication.ts index 1d50d3108..919980768 100644 --- a/packages/web/src/javascripts/Application/WebApplication.ts +++ b/packages/web/src/javascripts/Application/WebApplication.ts @@ -37,13 +37,13 @@ import { IsNativeIOS, IsNativeMobileWeb, KeyboardService, - PreferenceId, RouteServiceInterface, ThemeManager, VaultDisplayServiceInterface, WebAlertService, WebApplicationInterface, } from '@standardnotes/ui-services' +import { PreferencePaneId } from '@standardnotes/services' import { MobileWebReceiver, NativeMobileEventListener } from '../NativeMobileWeb/MobileWebReceiver' import { setCustomViewportHeight } from '@/setViewportHeightWithFallback' import { FeatureName } from '@/Controllers/FeatureName' @@ -504,7 +504,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter return this.environment === Environment.Web } - openPreferences(pane?: PreferenceId): void { + openPreferences(pane?: PreferencePaneId): void { this.preferencesController.openPreferences() if (pane) { this.preferencesController.setCurrentPane(pane) diff --git a/packages/web/src/javascripts/Components/Footer/Footer.tsx b/packages/web/src/javascripts/Components/Footer/Footer.tsx index 3fd353eaf..af70cd5c7 100644 --- a/packages/web/src/javascripts/Components/Footer/Footer.tsx +++ b/packages/web/src/javascripts/Components/Footer/Footer.tsx @@ -2,7 +2,7 @@ import { WebApplication } from '@/Application/WebApplication' import { WebApplicationGroup } from '@/Application/WebApplicationGroup' import { AbstractComponent } from '@/Components/Abstract/PureComponent' import { destroyAllObjectProperties, preventRefreshing } from '@/Utils' -import { ApplicationEvent, ApplicationDescriptor, WebAppEvent } from '@standardnotes/snjs' +import { ApplicationEvent, ApplicationDescriptor, WebAppEvent, StatusServiceEvent } from '@standardnotes/snjs' import { STRING_NEW_UPDATE_READY, STRING_CONFIRM_APP_QUIT_DURING_UPGRADE, @@ -112,7 +112,10 @@ class Footer extends AbstractComponent { override componentDidMount(): void { super.componentDidMount() - this.removeStatusObserver = this.application.status.addEventObserver((_event, message) => { + this.removeStatusObserver = this.application.status.addEventObserver((event, message) => { + if (event !== StatusServiceEvent.MessageChanged) { + return + } this.setState({ arbitraryStatusMessage: message, }) diff --git a/packages/web/src/javascripts/Components/Footer/PreferencesButton.tsx b/packages/web/src/javascripts/Components/Footer/PreferencesButton.tsx index 9d73ce221..cf3a543be 100644 --- a/packages/web/src/javascripts/Components/Footer/PreferencesButton.tsx +++ b/packages/web/src/javascripts/Components/Footer/PreferencesButton.tsx @@ -1,4 +1,4 @@ -import { compareSemVersions } from '@standardnotes/snjs' +import { compareSemVersions, StatusServiceEvent } from '@standardnotes/snjs' import { keyboardStringForShortcut, OPEN_PREFERENCES_COMMAND } from '@standardnotes/ui-services' import { useCallback, useEffect, useMemo, useState } from 'react' import { useApplication } from '../ApplicationProvider' @@ -36,11 +36,26 @@ const PreferencesButton = ({ openPreferences }: Props) => { openPreferences(isChangelogUnread) }, [isChangelogUnread, openPreferences]) + const [bubbleCount, setBubbleCount] = useState() + useEffect(() => { + return application.status.addEventObserver((event, message) => { + if (event !== StatusServiceEvent.PreferencesBubbleCountChanged) { + return + } + setBubbleCount(message) + }) + }, [application.status]) + return ( - diff --git a/packages/web/src/javascripts/Components/Preferences/Controller/PreferencesMenuItem.ts b/packages/web/src/javascripts/Components/Preferences/Controller/PreferencesMenuItem.ts index 5b1ba1eb0..dc3c11b59 100644 --- a/packages/web/src/javascripts/Components/Preferences/Controller/PreferencesMenuItem.ts +++ b/packages/web/src/javascripts/Components/Preferences/Controller/PreferencesMenuItem.ts @@ -1,10 +1,11 @@ import { IconType } from '@standardnotes/snjs' -import { PreferenceId } from '@standardnotes/ui-services' +import { PreferencePaneId } from '@standardnotes/services' export interface PreferencesMenuItem { - readonly id: PreferenceId + readonly id: PreferencePaneId readonly icon: IconType readonly label: string readonly order: number - readonly hasBubble?: boolean + readonly bubbleCount?: number + readonly hasErrorIndicator?: boolean } diff --git a/packages/web/src/javascripts/Components/Preferences/Controller/PreferencesSessionController.ts b/packages/web/src/javascripts/Components/Preferences/Controller/PreferencesSessionController.ts index bf820c581..e9e467aff 100644 --- a/packages/web/src/javascripts/Components/Preferences/Controller/PreferencesSessionController.ts +++ b/packages/web/src/javascripts/Components/Preferences/Controller/PreferencesSessionController.ts @@ -2,7 +2,7 @@ import { action, makeAutoObservable, observable } from 'mobx' import { WebApplication } from '@/Application/WebApplication' import { PackageProvider } from '../Panes/General/Advanced/Packages/Provider/PackageProvider' import { securityPrefsHasBubble } from '../Panes/Security/securityPrefsHasBubble' -import { PreferenceId } from '@standardnotes/ui-services' +import { PreferencePaneId } from '@standardnotes/services' import { isDesktopApplication } from '@/Utils' import { featureTrunkHomeServerEnabled, featureTrunkVaultsEnabled } from '@/FeatureTrunk' import { PreferencesMenuItem } from './PreferencesMenuItem' @@ -14,7 +14,7 @@ import { PREFERENCES_MENU_ITEMS, READY_PREFERENCES_MENU_ITEMS } from './MenuItem * Preferences menu. It is created and destroyed each time the menu is opened and closed. */ export class PreferencesSessionController { - private _selectedPane: PreferenceId = 'account' + private _selectedPane: PreferencePaneId = 'account' private _menu: PreferencesMenuItem[] private _extensionLatestVersions: PackageProvider = new PackageProvider(new Map()) @@ -69,7 +69,8 @@ export class PreferencesSessionController { const item: SelectableMenuItem = { ...preference, selected: preference.id === this._selectedPane, - hasBubble: this.sectionHasBubble(preference.id), + bubbleCount: this.application.status.getPreferencesBubbleCount(preference.id), + hasErrorIndicator: this.sectionHasBubble(preference.id), } return item }) @@ -81,7 +82,7 @@ export class PreferencesSessionController { return this._menu.find((item) => item.id === this._selectedPane) } - get selectedPaneId(): PreferenceId { + get selectedPaneId(): PreferencePaneId { if (this.selectedMenuItem != undefined) { return this.selectedMenuItem.id } @@ -89,11 +90,11 @@ export class PreferencesSessionController { return 'account' } - selectPane = (key: PreferenceId) => { + selectPane = (key: PreferencePaneId) => { this._selectedPane = key } - sectionHasBubble(id: PreferenceId): boolean { + sectionHasBubble(id: PreferencePaneId): boolean { if (id === 'security') { return securityPrefsHasBubble(this.application) } diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/ContactInviteModal.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/ContactInviteModal.tsx index f45a01abe..ca49debba 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/ContactInviteModal.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/ContactInviteModal.tsx @@ -80,24 +80,26 @@ const ContactInviteModal: FunctionComponent = ({ vault, onCloseDialog }) return ( -
+
{isLoadingContacts ? ( - - ) : ( + + ) : contacts.length > 0 ? ( contacts.map((contact) => { return ( ) }) + ) : ( +
No contacts available to invite.
)}
diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults.tsx index c7e884430..c95cf73c9 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults.tsx @@ -158,11 +158,13 @@ const Vaults = () => { Vaults -
- {vaults.map((vault) => { - return - })} -
+ {vaults.length > 0 && ( +
+ {vaults.map((vault) => { + return + })} +
+ )} {canCreateMoreVaults ? (