diff --git a/desktop/src/app/App.tsx b/desktop/src/app/App.tsx index 7316b010b..28790de36 100644 --- a/desktop/src/app/App.tsx +++ b/desktop/src/app/App.tsx @@ -39,7 +39,6 @@ function AppReady() { actions={onboarding.flow.actions} initialProfile={onboarding.flow.initialProfile} key={onboarding.currentPubkey ?? "anonymous"} - notifications={onboarding.flow.notifications} /> ); } diff --git a/desktop/src/features/notifications/hooks.ts b/desktop/src/features/notifications/hooks.ts index bb759695d..f4c9bac27 100644 --- a/desktop/src/features/notifications/hooks.ts +++ b/desktop/src/features/notifications/hooks.ts @@ -2,30 +2,22 @@ import * as React from "react"; import { useHomeFeedQuery } from "@/features/home/hooks"; import { useUsersBatchQuery } from "@/features/profile/hooks"; -import { - resolveUserLabel, - truncatePubkey, - type UserProfileLookup, -} from "@/features/profile/lib/identity"; -import type { FeedItem, HomeFeedResponse } from "@/shared/api/types"; -import { - collectHomeAlertItems, - eligibleFeedNotificationItems, - notificationBody, - notificationTitle, -} from "./lib/feed"; +import type { UserProfileLookup } from "@/features/profile/lib/identity"; +import type { HomeFeedResponse } from "@/shared/api/types"; import { getDesktopNotificationPermissionState, requestDesktopNotificationAccess, - sendDesktopNotification, type DesktopNotificationPermissionState, } from "./lib/desktop"; -import { playNotificationSound } from "./lib/sound"; +import { + readStoredSeenFeedIds, + useFeedDesktopNotifications, + writeStoredSeenFeedIds, +} from "./use-feed-desktop-notifications"; export type { DesktopNotificationPermissionState } from "./lib/desktop"; const NOTIFICATION_SETTINGS_STORAGE_KEY = "sprout-notification-settings.v1"; -const HOME_FEED_SEEN_STORAGE_KEY = "sprout-home-feed-seen.v1"; const HOME_FEED_SEEN_MAX_ITEMS = 500; export type NotificationSettings = { @@ -37,7 +29,7 @@ export type NotificationSettings = { }; const DEFAULT_NOTIFICATION_SETTINGS: NotificationSettings = { - desktopEnabled: false, + desktopEnabled: true, homeBadgeEnabled: true, mentions: true, needsAction: true, @@ -48,10 +40,6 @@ function notificationSettingsStorageKey(pubkey: string) { return `${NOTIFICATION_SETTINGS_STORAGE_KEY}:${pubkey}`; } -function homeFeedSeenStorageKey(pubkey: string) { - return `${HOME_FEED_SEEN_STORAGE_KEY}:${pubkey}`; -} - function sanitizeNotificationSettings(value: unknown): NotificationSettings { if (!value || typeof value !== "object") { return DEFAULT_NOTIFICATION_SETTINGS; @@ -115,41 +103,6 @@ function writeStoredNotificationSettings( ); } -function readStoredSeenFeedIds(pubkey: string): string[] { - if (typeof window === "undefined" || pubkey.length === 0) { - return []; - } - - const rawValue = window.localStorage.getItem(homeFeedSeenStorageKey(pubkey)); - if (!rawValue) { - return []; - } - - try { - const parsed = JSON.parse(rawValue); - if (!Array.isArray(parsed)) { - return []; - } - - return parsed - .filter((value): value is string => typeof value === "string") - .slice(-HOME_FEED_SEEN_MAX_ITEMS); - } catch { - return []; - } -} - -function writeStoredSeenFeedIds(pubkey: string, ids: string[]) { - if (typeof window === "undefined" || pubkey.length === 0) { - return; - } - - window.localStorage.setItem( - homeFeedSeenStorageKey(pubkey), - JSON.stringify(ids.slice(-HOME_FEED_SEEN_MAX_ITEMS)), - ); -} - function mergeSeenFeedIds(current: string[], nextIds: readonly string[]) { const merged = new Set(current); let didChange = false; @@ -300,125 +253,21 @@ export function useNotificationSettings(pubkey?: string) { }; } -export function useFeedDesktopNotifications( - feed: HomeFeedResponse | undefined, - pubkey: string | undefined, - settings: NotificationSettings, - profiles?: UserProfileLookup, -) { - const normalizedPubkey = pubkey?.trim().toLowerCase() ?? ""; - const seenItemIdsRef = React.useRef>( - new Set(readStoredSeenFeedIds(normalizedPubkey)), - ); - - React.useEffect(() => { - seenItemIdsRef.current = new Set(readStoredSeenFeedIds(normalizedPubkey)); - }, [normalizedPubkey]); - - const deliverFeedNotification = React.useEffectEvent( - async (item: FeedItem, senderName?: string) => { - const didSend = await sendDesktopNotification({ - body: notificationBody(item), - target: { - channelId: item.channelId, - channelName: item.channelName, - content: item.content, - createdAt: item.createdAt, - eventId: item.id, - kind: item.kind, - pubkey: item.pubkey, - }, - title: notificationTitle(item, senderName), - }); - - if (didSend && settings.soundEnabled) { - playNotificationSound(); - } - }, - ); - - React.useEffect(() => { - if (!feed) { - return; - } - - // Wait for sender profiles to load so notification titles include names. - // The first-load seed below marks all current items as seen, so we must - // defer it until profiles are available — otherwise items get marked seen - // before we can dispatch notifications with sender names. - if (profiles === undefined) { - return; - } - - const currentFeedItems = collectHomeAlertItems(feed); - - // Guard: empty seen set + populated feed means first load or cleared - // storage. Seed the seen set without notifying to prevent a flood. - if (seenItemIdsRef.current.size === 0 && currentFeedItems.length > 0) { - seenItemIdsRef.current = new Set(currentFeedItems.map((item) => item.id)); - writeStoredSeenFeedIds(normalizedPubkey, [...seenItemIdsRef.current]); - return; - } - - const nextSeenItemIds = new Set(seenItemIdsRef.current); - const newItems = settings.desktopEnabled - ? eligibleFeedNotificationItems(feed, { - mentions: settings.mentions, - needsAction: settings.needsAction, - }).filter((item) => !nextSeenItemIds.has(item.id)) - : []; - - for (const item of currentFeedItems) { - nextSeenItemIds.add(item.id); - } - - // Prevent unbounded growth — keep only the most recent entries. - if (nextSeenItemIds.size > HOME_FEED_SEEN_MAX_ITEMS) { - const excess = nextSeenItemIds.size - HOME_FEED_SEEN_MAX_ITEMS; - let removed = 0; - for (const id of nextSeenItemIds) { - if (removed >= excess) break; - nextSeenItemIds.delete(id); - removed++; - } - } - - seenItemIdsRef.current = nextSeenItemIds; - writeStoredSeenFeedIds(normalizedPubkey, [...nextSeenItemIds]); - - for (const item of newItems) { - const resolvedLabel = profiles - ? resolveUserLabel({ - pubkey: item.pubkey, - profiles, - preferResolvedSelfLabel: true, - }) - : undefined; - // Only use real display names, not truncated pubkey fallbacks. - const senderName = - resolvedLabel && resolvedLabel !== truncatePubkey(item.pubkey) - ? resolvedLabel - : undefined; - void deliverFeedNotification(item, senderName); - } - }, [ - feed, - normalizedPubkey, - profiles, - settings.desktopEnabled, - settings.mentions, - settings.needsAction, - ]); -} - export function useHomeFeedNotificationState( feed: HomeFeedResponse | undefined, pubkey: string | undefined, settings: NotificationSettings, + setDesktopEnabled: (enabled: boolean) => Promise, isHomeActive: boolean, profiles?: UserProfileLookup, ) { - useFeedDesktopNotifications(feed, pubkey, settings, profiles); + useFeedDesktopNotifications( + feed, + pubkey, + settings, + setDesktopEnabled, + profiles, + ); const normalizedPubkey = pubkey?.trim().toLowerCase() ?? ""; const [seenFeedIds, setSeenFeedIds] = React.useState(() => readStoredSeenFeedIds(normalizedPubkey), @@ -519,6 +368,7 @@ export function useHomeFeedNotifications( homeFeedQuery.data, pubkey, notificationSettings.settings, + notificationSettings.setDesktopEnabled, isHomeActive, feedProfilesQuery.data?.profiles, ); diff --git a/desktop/src/features/notifications/lib/desktop.ts b/desktop/src/features/notifications/lib/desktop.ts index 1dfeec4e0..384f6da80 100644 --- a/desktop/src/features/notifications/lib/desktop.ts +++ b/desktop/src/features/notifications/lib/desktop.ts @@ -124,12 +124,23 @@ export async function getDesktopNotificationPermissionState(): Promise | null = + null; + export async function requestDesktopNotificationAccess(): Promise { if (!hasNotificationApi()) { return "unsupported"; } - return requestPermission(); + if (pendingPermissionRequest) { + return pendingPermissionRequest; + } + + pendingPermissionRequest = requestPermission().finally(() => { + pendingPermissionRequest = null; + }); + + return pendingPermissionRequest; } export async function listenForDesktopNotificationActions( diff --git a/desktop/src/features/notifications/use-feed-desktop-notifications.ts b/desktop/src/features/notifications/use-feed-desktop-notifications.ts new file mode 100644 index 000000000..da02f625a --- /dev/null +++ b/desktop/src/features/notifications/use-feed-desktop-notifications.ts @@ -0,0 +1,198 @@ +import * as React from "react"; + +import { + resolveUserLabel, + truncatePubkey, + type UserProfileLookup, +} from "@/features/profile/lib/identity"; +import type { FeedItem, HomeFeedResponse } from "@/shared/api/types"; +import { + collectHomeAlertItems, + eligibleFeedNotificationItems, + notificationBody, + notificationTitle, +} from "./lib/feed"; +import { + getDesktopNotificationPermissionState, + requestDesktopNotificationAccess, + sendDesktopNotification, +} from "./lib/desktop"; +import { playNotificationSound } from "./lib/sound"; +import type { NotificationSettings } from "./hooks"; + +const HOME_FEED_SEEN_STORAGE_KEY = "sprout-home-feed-seen.v1"; +const HOME_FEED_SEEN_MAX_ITEMS = 500; + +function homeFeedSeenStorageKey(pubkey: string) { + return `${HOME_FEED_SEEN_STORAGE_KEY}:${pubkey}`; +} + +export function readStoredSeenFeedIds(pubkey: string): string[] { + if (typeof window === "undefined" || pubkey.length === 0) { + return []; + } + + const rawValue = window.localStorage.getItem(homeFeedSeenStorageKey(pubkey)); + if (!rawValue) { + return []; + } + + try { + const parsed = JSON.parse(rawValue); + if (!Array.isArray(parsed)) { + return []; + } + + return parsed + .filter((value): value is string => typeof value === "string") + .slice(-HOME_FEED_SEEN_MAX_ITEMS); + } catch { + return []; + } +} + +export function writeStoredSeenFeedIds(pubkey: string, ids: string[]) { + if (typeof window === "undefined" || pubkey.length === 0) { + return; + } + + window.localStorage.setItem( + homeFeedSeenStorageKey(pubkey), + JSON.stringify(ids.slice(-HOME_FEED_SEEN_MAX_ITEMS)), + ); +} + +export function useFeedDesktopNotifications( + feed: HomeFeedResponse | undefined, + pubkey: string | undefined, + settings: NotificationSettings, + setDesktopEnabled: (enabled: boolean) => Promise, + profiles?: UserProfileLookup, +) { + const normalizedPubkey = pubkey?.trim().toLowerCase() ?? ""; + const seenItemIdsRef = React.useRef>( + new Set(readStoredSeenFeedIds(normalizedPubkey)), + ); + const hasAutoRequestedRef = React.useRef(false); + + React.useEffect(() => { + seenItemIdsRef.current = new Set(readStoredSeenFeedIds(normalizedPubkey)); + hasAutoRequestedRef.current = false; + }, [normalizedPubkey]); + + const autoRequestPermissionIfNeeded = React.useEffectEvent(async () => { + if (hasAutoRequestedRef.current) { + return; + } + + const currentPermission = await getDesktopNotificationPermissionState(); + if (currentPermission !== "default") { + return; + } + + hasAutoRequestedRef.current = true; + const result = await requestDesktopNotificationAccess(); + if (result !== "granted") { + void setDesktopEnabled(false); + } + }); + + const deliverFeedNotification = React.useEffectEvent( + async (item: FeedItem, senderName?: string) => { + const didSend = await sendDesktopNotification({ + body: notificationBody(item), + target: { + channelId: item.channelId, + channelName: item.channelName, + content: item.content, + createdAt: item.createdAt, + eventId: item.id, + kind: item.kind, + pubkey: item.pubkey, + }, + title: notificationTitle(item, senderName), + }); + + if (didSend && settings.soundEnabled) { + playNotificationSound(); + } + }, + ); + + React.useEffect(() => { + if (!feed) { + return; + } + + // Wait for sender profiles to load so notification titles include names. + // The first-load seed below marks all current items as seen, so we must + // defer it until profiles are available — otherwise items get marked seen + // before we can dispatch notifications with sender names. + if (profiles === undefined) { + return; + } + + const currentFeedItems = collectHomeAlertItems(feed); + + // Guard: empty seen set + populated feed means first load or cleared + // storage. Seed the seen set without notifying to prevent a flood. + if (seenItemIdsRef.current.size === 0 && currentFeedItems.length > 0) { + seenItemIdsRef.current = new Set(currentFeedItems.map((item) => item.id)); + writeStoredSeenFeedIds(normalizedPubkey, [...seenItemIdsRef.current]); + return; + } + + const nextSeenItemIds = new Set(seenItemIdsRef.current); + const newItems = settings.desktopEnabled + ? eligibleFeedNotificationItems(feed, { + mentions: settings.mentions, + needsAction: settings.needsAction, + }).filter((item) => !nextSeenItemIds.has(item.id)) + : []; + + for (const item of currentFeedItems) { + nextSeenItemIds.add(item.id); + } + + // Prevent unbounded growth — keep only the most recent entries. + if (nextSeenItemIds.size > HOME_FEED_SEEN_MAX_ITEMS) { + const excess = nextSeenItemIds.size - HOME_FEED_SEEN_MAX_ITEMS; + let removed = 0; + for (const id of nextSeenItemIds) { + if (removed >= excess) break; + nextSeenItemIds.delete(id); + removed++; + } + } + + seenItemIdsRef.current = nextSeenItemIds; + writeStoredSeenFeedIds(normalizedPubkey, [...nextSeenItemIds]); + + if (newItems.length > 0) { + void autoRequestPermissionIfNeeded(); + } + + for (const item of newItems) { + const resolvedLabel = profiles + ? resolveUserLabel({ + pubkey: item.pubkey, + profiles, + preferResolvedSelfLabel: true, + }) + : undefined; + // Only use real display names, not truncated pubkey fallbacks. + const senderName = + resolvedLabel && resolvedLabel !== truncatePubkey(item.pubkey) + ? resolvedLabel + : undefined; + void deliverFeedNotification(item, senderName); + } + }, [ + feed, + normalizedPubkey, + profiles, + settings.desktopEnabled, + settings.mentions, + settings.needsAction, + ]); +} diff --git a/desktop/src/features/onboarding/hooks.ts b/desktop/src/features/onboarding/hooks.ts index 18c94249d..3b4c4e5a1 100644 --- a/desktop/src/features/onboarding/hooks.ts +++ b/desktop/src/features/onboarding/hooks.ts @@ -2,7 +2,6 @@ import * as React from "react"; import { useQueryClient, type QueryStatus } from "@tanstack/react-query"; import { channelsQueryKey } from "@/features/channels/hooks"; -import { useNotificationSettings } from "@/features/notifications/hooks"; import { useProfileQuery } from "@/features/profile/hooks"; import { useIdentityQuery } from "@/shared/api/hooks"; import { getChannels, joinChannel } from "@/shared/api/tauri"; @@ -220,7 +219,6 @@ export function useAppOnboardingState() { const identity = identityQuery.data; const currentPubkey = identity?.pubkey ?? null; const profileQuery = useProfileQuery(); - const notificationState = useNotificationSettings(currentPubkey ?? undefined); const onboardingGate = useFirstRunOnboardingGate({ currentPubkey, identityIsFetching: identityQuery.fetchStatus === "fetching", @@ -240,13 +238,6 @@ export function useAppOnboardingState() { initialProfile: { profile: profileQuery.data, }, - notifications: { - errorMessage: notificationState.errorMessage, - isUpdatingDesktopEnabled: notificationState.isUpdatingDesktopEnabled, - permission: notificationState.permission, - setDesktopEnabled: notificationState.setDesktopEnabled, - settings: notificationState.settings, - }, }; return { diff --git a/desktop/src/features/onboarding/ui/OnboardingFlow.tsx b/desktop/src/features/onboarding/ui/OnboardingFlow.tsx index 96d66c306..97387f9c0 100644 --- a/desktop/src/features/onboarding/ui/OnboardingFlow.tsx +++ b/desktop/src/features/onboarding/ui/OnboardingFlow.tsx @@ -19,7 +19,6 @@ import { ProfileStep } from "./ProfileStep"; import { SetupStep } from "./SetupStep"; import type { OnboardingActions, - OnboardingNotifications, OnboardingPage, OnboardingProfileSeed, OnboardingProfileValues, @@ -63,7 +62,6 @@ async function checkMembershipDenied(): Promise { type OnboardingFlowProps = { actions: OnboardingActions; initialProfile: OnboardingProfileSeed; - notifications: OnboardingNotifications; }; function isFallbackDisplayName(value?: string | null) { @@ -131,10 +129,8 @@ function resolveProfileSaveRecovery( export function OnboardingFlow({ actions, initialProfile, - notifications, }: OnboardingFlowProps) { const { complete, skipForNow } = actions; - const { setDesktopEnabled } = notifications; const savedProfile = resolveSavedProfile(initialProfile); const profileUpdateMutation = useUpdateProfileMutation(); const { error: profileSaveError, isPending: isSavingProfile } = @@ -259,9 +255,6 @@ export function OnboardingFlow({ updateProfileDraft({ avatarUrl: savedProfile.avatarUrl }); }, [savedProfile.avatarUrl, updateProfileDraft]); - const handleEnableDesktopNotifications = React.useCallback(() => { - void setDesktopEnabled(true); - }, [setDesktopEnabled]); const saveErrorMessage = profileSaveError instanceof Error ? profileSaveError.message : null; const profileStepState: ProfileStepState = { @@ -360,9 +353,7 @@ export function OnboardingFlow({ actions={{ back: showProfilePage, complete, - enableDesktopNotifications: handleEnableDesktopNotifications, }} - notifications={notifications} /> )} diff --git a/desktop/src/features/onboarding/ui/ProfileStep.tsx b/desktop/src/features/onboarding/ui/ProfileStep.tsx index 67a205e8f..c784c599d 100644 --- a/desktop/src/features/onboarding/ui/ProfileStep.tsx +++ b/desktop/src/features/onboarding/ui/ProfileStep.tsx @@ -1,5 +1,13 @@ import * as React from "react"; -import { Check, KeyRound, Loader2, Upload, UserRound } from "lucide-react"; +import { + Check, + Copy, + KeyRound, + Loader2, + Upload, + UserRound, +} from "lucide-react"; +import { toast } from "sonner"; import { AvatarUpload } from "@/features/profile/ui/AvatarUpload"; import { nsecToNpub, shortenNpub } from "@/shared/lib/nostrUtils"; @@ -272,25 +280,6 @@ export function ProfileStep({ actions, state }: ProfileStepProps) {

Set up your profile

-

- Add the name people will see in Sprout. A photo is optional, but it - helps people spot you faster. -

- {/* Show the active identity so the user can confirm which key - they're saving the profile for — and so it's obvious when - they need to swap to a different key (e.g. an allowlisted - one for a gated relay). */} - {currentNpub ? ( -

- You are{" "} - - {shortenNpub(currentNpub)} - -

- ) : null} @@ -320,9 +309,6 @@ export function ProfileStep({ actions, state }: ProfileStepProps) { value={displayNameDraft} /> -

- You can change this later in Profile settings. -

0 && avatar.draftUrl !== avatar.savedUrl } disabled={isSaving} + idleHint="" testIdPrefix="onboarding-avatar" /> @@ -342,7 +329,29 @@ export function ProfileStep({ actions, state }: ProfileStepProps) { -
+
+ {currentNpub ? ( + <> +

+ {shortenNpub(currentNpub)} +

+ + + ) : null} +
{saveRecovery.canSkipForNow ? ( - - {notifications.errorMessage ? ( -

- {notifications.errorMessage} -

- ) : null} -
-
@@ -246,8 +139,8 @@ function SetupStepContent({ actions, state }: SetupStepContentProps) { ); } -export function SetupStep({ actions, notifications }: SetupStepProps) { - const state = useSetupStepState(notifications); +export function SetupStep({ actions }: SetupStepProps) { + const state = useSetupStepState(); return ; } diff --git a/desktop/src/features/onboarding/ui/types.ts b/desktop/src/features/onboarding/ui/types.ts index cae551fd5..18468491a 100644 --- a/desktop/src/features/onboarding/ui/types.ts +++ b/desktop/src/features/onboarding/ui/types.ts @@ -1,7 +1,3 @@ -import type { - DesktopNotificationPermissionState, - NotificationSettings, -} from "@/features/notifications/hooks"; import type { AcpProvider, Profile } from "@/shared/api/types"; export type OnboardingPage = "profile" | "setup" | "membership-denied"; @@ -20,14 +16,6 @@ export type OnboardingProfileValues = { displayName: string; }; -export type OnboardingNotifications = { - errorMessage: string | null; - isUpdatingDesktopEnabled: boolean; - permission: DesktopNotificationPermissionState; - setDesktopEnabled: (enabled: boolean) => Promise; - settings: NotificationSettings; -}; - export type ProfileStepSaveRecovery = { canAdvanceWithoutSaving: boolean; canSkipForNow: boolean; @@ -69,7 +57,6 @@ export type ProfileStepActions = { export type SetupStepActions = { back: () => void; complete: () => void; - enableDesktopNotifications: () => void; }; export type SetupStepRuntimeState = { @@ -80,6 +67,5 @@ export type SetupStepRuntimeState = { }; export type SetupStepState = { - notifications: OnboardingNotifications; runtimeProviders: SetupStepRuntimeState; }; diff --git a/desktop/src/features/profile/ui/AvatarUpload.tsx b/desktop/src/features/profile/ui/AvatarUpload.tsx index 990e239a1..1326ad90b 100644 --- a/desktop/src/features/profile/ui/AvatarUpload.tsx +++ b/desktop/src/features/profile/ui/AvatarUpload.tsx @@ -1,9 +1,8 @@ import * as React from "react"; -import { Camera, Link2, Loader2 } from "lucide-react"; +import { Camera, Link2, Loader2, Upload, X } from "lucide-react"; import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar"; import { useAvatarUpload } from "@/features/profile/useAvatarUpload"; -import { Button } from "@/shared/ui/button"; import { Input } from "@/shared/ui/input"; type AvatarUploadProps = { @@ -26,9 +25,11 @@ export function AvatarUpload({ onUploadingChange, showClear, disabled, - idleHint = "You can always add one later.", + idleHint = "", testIdPrefix = "avatar", }: AvatarUploadProps) { + const [isDragging, setIsDragging] = React.useState(false); + const onUploadSuccess = React.useCallback( (url: string) => { onUrlChange(url); @@ -51,68 +52,113 @@ export function AvatarUpload({ const isInputDisabled = disabled || isUploading; - return ( -
-
-
-
- -
- -
-
-
-

Add a profile photo

-

- Optional, but it makes conversations easier to scan. -

-
-
+ const handleDrop = React.useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + const file = e.dataTransfer.files[0]; + if (file && inputRef.current) { + const dt = new DataTransfer(); + dt.items.add(file); + inputRef.current.files = dt.files; + void handleFileChange({ + target: inputRef.current, + } as React.ChangeEvent); + } + }, + [inputRef, handleFileChange], + ); -
- + return ( +
+

Add a profile photo

+
+
+ {showClear && onClear ? ( - + + ) : ( -

{idleHint}

+
+ +
)} - { - void handleFileChange(event); - }} - ref={inputRef} - type="file" - />
+ + { + void handleFileChange(event); + }} + ref={inputRef} + type="file" + />
+ {idleHint ? ( +

{idleHint}

+ ) : null} -
+
@@ -132,12 +178,12 @@ export function AvatarUpload({ />

- Prefer a link instead? Paste it here and we'll save that instead. + Or paste a direct image URL.

{errorMessage ? ( -

+

{errorMessage}

) : null} diff --git a/desktop/src/features/settings/ui/NotificationSettingsCard.tsx b/desktop/src/features/settings/ui/NotificationSettingsCard.tsx index 92374ac3c..e465c980a 100644 --- a/desktop/src/features/settings/ui/NotificationSettingsCard.tsx +++ b/desktop/src/features/settings/ui/NotificationSettingsCard.tsx @@ -34,8 +34,7 @@ export function NotificationSettingsCard({

Notifications

- Zero by default. Keep channel noise inside Home, then opt in to - desktop alerts only for the items that truly need you. + Desktop alerts are on by default. Fine-tune what gets through below.

diff --git a/desktop/tests/e2e/integration.spec.ts b/desktop/tests/e2e/integration.spec.ts index ff87f8e41..61fe9296b 100644 --- a/desktop/tests/e2e/integration.spec.ts +++ b/desktop/tests/e2e/integration.spec.ts @@ -34,12 +34,11 @@ async function closeChannelManagement(page: import("@playwright/test").Page) { await expect(page.getByTestId("channel-management-sheet")).not.toBeVisible(); } -async function enableDesktopNotifications( +async function assertDesktopNotificationsEnabled( page: import("@playwright/test").Page, ) { await openSettings(page, "notifications"); await expect(page.getByTestId("settings-notifications")).toBeVisible(); - await page.getByTestId("notifications-desktop-toggle").click(); await expect(page.getByTestId("notifications-desktop-state")).toContainText( "On", ); @@ -261,7 +260,7 @@ test("live mentions refetch the home feed without waiting for polling", async ({ await targetPage.goto("/"); await senderPage.goto("/"); - await enableDesktopNotifications(targetPage); + await assertDesktopNotificationsEnabled(targetPage); await targetPage.getByTestId("channel-general").click(); await expect(targetPage.getByTestId("chat-title")).toHaveText("general"); @@ -326,7 +325,7 @@ test("live forum mentions refetch the home feed without waiting for polling", as await targetPage.goto("/"); await senderPage.goto("/"); - await enableDesktopNotifications(targetPage); + await assertDesktopNotificationsEnabled(targetPage); await targetPage.getByTestId("channel-general").click(); await expect(targetPage.getByTestId("chat-title")).toHaveText("general"); diff --git a/desktop/tests/e2e/onboarding.spec.ts b/desktop/tests/e2e/onboarding.spec.ts index 5bc58a88c..2a8283d0d 100644 --- a/desktop/tests/e2e/onboarding.spec.ts +++ b/desktop/tests/e2e/onboarding.spec.ts @@ -108,7 +108,7 @@ test("identity fallback text does not count as a real onboarding name", async ({ await expectIncompleteOnboarding(page); await expect(page.getByTestId("onboarding-avatar-upload")).toHaveText( - "Upload photo", + "Drop an image or browse", ); await expect(page.getByTestId("onboarding-avatar-url")).toHaveValue(""); await expect(page.getByTestId("onboarding-next")).toBeDisabled(); diff --git a/desktop/tests/e2e/profile.spec.ts b/desktop/tests/e2e/profile.spec.ts index 143d95e14..59e74cedb 100644 --- a/desktop/tests/e2e/profile.spec.ts +++ b/desktop/tests/e2e/profile.spec.ts @@ -85,14 +85,10 @@ test("notification settings drive the Home badge and desktop alerts", async ({ await openSettings(page, "notifications"); await expect(page.getByTestId("settings-notifications")).toBeVisible(); - await expect(page.getByTestId("notifications-desktop-state")).toContainText( - "Off", - ); - - await page.getByTestId("notifications-desktop-toggle").click(); await expect(page.getByTestId("notifications-desktop-state")).toContainText( "On", ); + await page.getByTestId("settings-close").click(); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); @@ -207,7 +203,6 @@ test("desktop notification clicks open the matching forum thread", async ({ await page.goto("/"); await openSettings(page, "notifications"); - await page.getByTestId("notifications-desktop-toggle").click(); await expect(page.getByTestId("notifications-desktop-state")).toContainText( "On", );