diff --git a/CHANGELOG.md b/CHANGELOG.md index fca3234ad..64902570c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved naming of a couple list-related classes. - Track TestFlight vs AppStore installations in Posthog. [#130](https://github.com/verse-pbc/issues/issues/130) - Track breadcrumbs in Sentry for all analytics events. [#125](https://github.com/verse-pbc/issues/issues/125) +- Refactored the way the ProfileView downloads data and logs analytics events. [#1748](https://github.com/planetary-social/nos/pull/1748) ## [1.1] - 2025-01-03Z diff --git a/Nos.xcodeproj/project.pbxproj b/Nos.xcodeproj/project.pbxproj index 82a7b4ca2..4ddedaf76 100644 --- a/Nos.xcodeproj/project.pbxproj +++ b/Nos.xcodeproj/project.pbxproj @@ -413,6 +413,7 @@ C98298332ADD7F9A0096C5B5 /* DeepLinkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98298322ADD7F9A0096C5B5 /* DeepLinkService.swift */; }; C98298342ADD7F9A0096C5B5 /* DeepLinkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98298322ADD7F9A0096C5B5 /* DeepLinkService.swift */; }; C98651102B0BD49200597B68 /* PagedNoteListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C986510F2B0BD49200597B68 /* PagedNoteListView.swift */; }; + C987153D2D4D198200EA2F56 /* OnTabAppearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C987153C2D4D198200EA2F56 /* OnTabAppearModifier.swift */; }; C987F81729BA4C6A00B44E7A /* BigActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C987F81629BA4C6900B44E7A /* BigActionButton.swift */; }; C987F81A29BA4D0E00B44E7A /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C987F81929BA4D0E00B44E7A /* ActionButton.swift */; }; C987F81D29BA6D9A00B44E7A /* ProfileTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = C987F81C29BA6D9A00B44E7A /* ProfileTab.swift */; }; @@ -951,6 +952,7 @@ C98298312ADD7EDB0096C5B5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; C98298322ADD7F9A0096C5B5 /* DeepLinkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkService.swift; sourceTree = ""; }; C986510F2B0BD49200597B68 /* PagedNoteListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagedNoteListView.swift; sourceTree = ""; }; + C987153C2D4D198200EA2F56 /* OnTabAppearModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnTabAppearModifier.swift; sourceTree = ""; }; C987F81629BA4C6900B44E7A /* BigActionButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BigActionButton.swift; sourceTree = ""; }; C987F81929BA4D0E00B44E7A /* ActionButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = ""; }; C987F81C29BA6D9A00B44E7A /* ProfileTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTab.swift; sourceTree = ""; }; @@ -1868,12 +1870,13 @@ 03C7E7A12CB9CD0B0054624C /* PointDownEmojiTipViewStyle.swift */, C9A25B3C29F174D200B39534 /* ReadabilityPadding.swift */, C9E37E0E2A1E7C32003D4B0A /* ReportMenuModifier.swift */, + C987153C2D4D198200EA2F56 /* OnTabAppearModifier.swift */, C9A0DAE629C69FA000466635 /* Text+Gradient.swift */, + 04C9D7262CBF09C200EAAD4D /* TextField+PlaceHolderStyle.swift */, C9DC6CB92C1739AD00E1CFB3 /* View+HandleURLsInRouter.swift */, 50089A162C98678600834588 /* View+ListRowGradientBackground.swift */, C93EC2FC29C3785C0012EE2A /* View+RoundedCorner.swift */, 50DE6B1A2C6B88FE0065665D /* View+StyledBorder.swift */, - 04C9D7262CBF09C200EAAD4D /* TextField+PlaceHolderStyle.swift */, ); path = Modifiers; sourceTree = ""; @@ -2261,7 +2264,7 @@ C9B737702AB24D5F00398BE7 /* XCRemoteSwiftPackageReference "SwiftGenPlugin" */, C91565BF2B2368FA0068EECA /* XCRemoteSwiftPackageReference "ViewInspector" */, 3AD3185B2B294E6200026B07 /* XCRemoteSwiftPackageReference "xcstrings-tool-plugin" */, - C9FD34F42BCEC89C008F8D95 /* XCRemoteSwiftPackageReference "secp256k1.swift" */, + C9FD34F42BCEC89C008F8D95 /* XCRemoteSwiftPackageReference "secp256k1" */, C9FD35112BCED5A6008F8D95 /* XCRemoteSwiftPackageReference "nostr-sdk-ios" */, 03C49ABE2C938A9C00502321 /* XCRemoteSwiftPackageReference "SwiftSoup" */, 039389212CA4985C00698978 /* XCRemoteSwiftPackageReference "SDWebImageWebPCoder" */, @@ -2575,7 +2578,6 @@ C98CA9042B14FA3D00929141 /* PagedRelaySubscription.swift in Sources */, 5B0D99032A94090A0039F0C5 /* DoubleTapToPopModifier.swift in Sources */, 030024192CC00DFC0073ED56 /* SplashScreenView.swift in Sources */, - 50CBD8102D3A8B7000BF8A0B /* ListCircle.swift in Sources */, 0357299B2BE415E5005FEE85 /* ContentWarningController.swift in Sources */, 5BFF66B42A58853D00AA79DD /* PublishedEventsView.swift in Sources */, 03D1B42C2C3C1B0D001778CD /* TLVElement.swift in Sources */, @@ -2626,6 +2628,7 @@ 65BD8DC22BDAF2C300802039 /* DiscoverTab.swift in Sources */, 65BD8DC32BDAF2C300802039 /* FeaturedAuthorCategory.swift in Sources */, C93F045E2B9B7A7000AD5872 /* ReplyPreview.swift in Sources */, + C987153D2D4D198200EA2F56 /* OnTabAppearModifier.swift in Sources */, C97465312A3B89140031226F /* AuthorLabel.swift in Sources */, C9C547592A4F1D8C006B0741 /* NosNotification+CoreDataClass.swift in Sources */, 030AE4292BE3D63C004DEE02 /* FeaturedAuthor.swift in Sources */, @@ -2928,11 +2931,11 @@ /* Begin PBXTargetDependency section */ 3AD3185D2B294E9000026B07 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - productRef = 3AD3185C2B294E9000026B07 /* plugin:XCStringsToolPlugin */; + productRef = 3AD3185C2B294E9000026B07 /* XCStringsToolPlugin */; }; 3AEABEF32B2BF806001BC933 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - productRef = 3AEABEF22B2BF806001BC933 /* plugin:XCStringsToolPlugin */; + productRef = 3AEABEF22B2BF806001BC933 /* XCStringsToolPlugin */; }; C90862C229E9804B00C35A71 /* PBXTargetDependency */ = { isa = PBXTargetDependency; @@ -2941,11 +2944,11 @@ }; C9A6C7442AD83F7A001F9500 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - productRef = C9A6C7432AD83F7A001F9500 /* plugin:SwiftGenPlugin */; + productRef = C9A6C7432AD83F7A001F9500 /* SwiftGenPlugin */; }; C9D573402AB24A3700E06BB4 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - productRef = C9D5733F2AB24A3700E06BB4 /* plugin:SwiftGenPlugin */; + productRef = C9D5733F2AB24A3700E06BB4 /* SwiftGenPlugin */; }; C9DEBFE6298941020078B43A /* PBXTargetDependency */ = { isa = PBXTargetDependency; @@ -3819,7 +3822,7 @@ minimumVersion = 4.0.0; }; }; - C9FD34F42BCEC89C008F8D95 /* XCRemoteSwiftPackageReference "secp256k1.swift" */ = { + C9FD34F42BCEC89C008F8D95 /* XCRemoteSwiftPackageReference "secp256k1" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/GigaBitcoin/secp256k1.swift"; requirement = { @@ -3858,12 +3861,12 @@ package = 03C49ABE2C938A9C00502321 /* XCRemoteSwiftPackageReference "SwiftSoup" */; productName = SwiftSoup; }; - 3AD3185C2B294E9000026B07 /* plugin:XCStringsToolPlugin */ = { + 3AD3185C2B294E9000026B07 /* XCStringsToolPlugin */ = { isa = XCSwiftPackageProductDependency; package = 3AD3185B2B294E6200026B07 /* XCRemoteSwiftPackageReference "xcstrings-tool-plugin" */; productName = "plugin:XCStringsToolPlugin"; }; - 3AEABEF22B2BF806001BC933 /* plugin:XCStringsToolPlugin */ = { + 3AEABEF22B2BF806001BC933 /* XCStringsToolPlugin */ = { isa = XCSwiftPackageProductDependency; package = 3AD3185B2B294E6200026B07 /* XCRemoteSwiftPackageReference "xcstrings-tool-plugin" */; productName = "plugin:XCStringsToolPlugin"; @@ -3942,7 +3945,7 @@ package = C99DBF7C2A9E81CF00F7068F /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */; productName = SDWebImageSwiftUI; }; - C9A6C7432AD83F7A001F9500 /* plugin:SwiftGenPlugin */ = { + C9A6C7432AD83F7A001F9500 /* SwiftGenPlugin */ = { isa = XCSwiftPackageProductDependency; package = C9B737702AB24D5F00398BE7 /* XCRemoteSwiftPackageReference "SwiftGenPlugin" */; productName = "plugin:SwiftGenPlugin"; @@ -3962,7 +3965,7 @@ package = C9B71DBC2A8E9BAD0031ED9F /* XCRemoteSwiftPackageReference "sentry-cocoa" */; productName = Sentry; }; - C9D5733F2AB24A3700E06BB4 /* plugin:SwiftGenPlugin */ = { + C9D5733F2AB24A3700E06BB4 /* SwiftGenPlugin */ = { isa = XCSwiftPackageProductDependency; package = C9C8450C2AB249DB00654BC1 /* XCRemoteSwiftPackageReference "SwiftGenPlugin" */; productName = "plugin:SwiftGenPlugin"; @@ -3974,12 +3977,12 @@ }; C9FD34F52BCEC89C008F8D95 /* secp256k1 */ = { isa = XCSwiftPackageProductDependency; - package = C9FD34F42BCEC89C008F8D95 /* XCRemoteSwiftPackageReference "secp256k1.swift" */; + package = C9FD34F42BCEC89C008F8D95 /* XCRemoteSwiftPackageReference "secp256k1" */; productName = secp256k1; }; C9FD34F72BCEC8B5008F8D95 /* secp256k1 */ = { isa = XCSwiftPackageProductDependency; - package = C9FD34F42BCEC89C008F8D95 /* XCRemoteSwiftPackageReference "secp256k1.swift" */; + package = C9FD34F42BCEC89C008F8D95 /* XCRemoteSwiftPackageReference "secp256k1" */; productName = secp256k1; }; C9FD35122BCED5A6008F8D95 /* NostrSDK */ = { diff --git a/Nos/Router.swift b/Nos/Router.swift index c9761e8ab..8a368a4ce 100644 --- a/Nos/Router.swift +++ b/Nos/Router.swift @@ -17,6 +17,7 @@ import Dependencies @Dependency(\.persistenceController) private var persistenceController @Dependency(\.relayService) private var relayService @Dependency(\.crashReporting) private var crashReporting + @Dependency(\.analytics) private var analytics /// The `NavigationPath` of the tab (or side menu) the user currently has open. /// This has to be a two-way binding, but really the only things that should be modifying it are the `Router` @@ -104,6 +105,7 @@ import Dependencies /// Pushes a profile view for the given author. func push(_ author: Author) { + analytics.showedProfile() push(.author(author.hexadecimalPublicKey)) } diff --git a/Nos/Service/Analytics.swift b/Nos/Service/Analytics.swift index ad7171368..6c9994150 100644 --- a/Nos/Service/Analytics.swift +++ b/Nos/Service/Analytics.swift @@ -48,11 +48,11 @@ class Analytics { } func showedHome() { - track("Home Tab Tapped") + track("Home Tab Opened") } func showedDiscover() { - track("Discover Tab Tapped") + track("Discover Tab Opened") } func showedNoteComposer() { @@ -60,13 +60,17 @@ class Analytics { } func showedNotifications() { - track("Notifications Tab Tapped") + track("Notifications Tab Opened") } func showedProfile() { track("Profile View Opened") } + func showedProfileTab() { + track("Profile Tab Opened") + } + func showedThread() { track("Thread View Opened") } diff --git a/Nos/Views/AppView.swift b/Nos/Views/AppView.swift index f6ec3abf1..b2f6d2285 100644 --- a/Nos/Views/AppView.swift +++ b/Nos/Views/AppView.swift @@ -126,7 +126,7 @@ struct AppView: View { .badge(pushNotificationService.badgeCount) if let author = currentUser.author { - ProfileTab(author: author, path: $router.profilePath) + ProfileTab(author: author) .tabItem { VStack { let text = Text("profileTitle") diff --git a/Nos/Views/Discover/DiscoverTab.swift b/Nos/Views/Discover/DiscoverTab.swift index 749289a6c..a3e56b364 100644 --- a/Nos/Views/Discover/DiscoverTab.swift +++ b/Nos/Views/Discover/DiscoverTab.swift @@ -15,8 +15,6 @@ struct DiscoverTab: View { @State var columns: Int = 0 - @State private var isVisible = false - @State private var searchController = SearchController() @FocusState private var isSearching: Bool @@ -56,18 +54,8 @@ struct DiscoverTab: View { } .background(Color.appBg) .animation(.easeInOut, value: columns) - .onAppear { - if router.selectedTab == .discover { - isVisible = true - } - } - .onDisappear { - isVisible = false - } - .onChange(of: isVisible) { - if isVisible { - analytics.showedDiscover() - } + .onTabAppear(.discover) { + analytics.showedDiscover() } .nosNavigationBar("discover") .toolbarBackground(.visible, for: .navigationBar) diff --git a/Nos/Views/Home/HomeFeedView.swift b/Nos/Views/Home/HomeFeedView.swift index 86b134608..a840a25cb 100644 --- a/Nos/Views/Home/HomeFeedView.swift +++ b/Nos/Views/Home/HomeFeedView.swift @@ -9,11 +9,10 @@ struct HomeFeedView: View { @Environment(\.managedObjectContext) private var viewContext @EnvironmentObject private var router: Router - @ObservationIgnored @Dependency(\.analytics) private var analytics + @Dependency(\.analytics) private var analytics @ObserveInjection var inject @State private var refreshController = RefreshController(lastRefreshDate: Date.now + Self.staticLoadTime) - @State private var isVisible = false @State private var feedController: FeedController /// When set to true this will display a fullscreen progress wheel for a set amount of time to give us a chance @@ -188,17 +187,9 @@ struct HomeFeedView: View { .navigationBarTitle("", displayMode: .inline) .padding(.top, 1) .environment(feedController) - .onAppear { - if router.selectedTab == .home { - isVisible = true - } - } - .onDisappear { isVisible = false } - .onChange(of: isVisible) { - if isVisible { - analytics.showedHome() - GoToFeedTip.viewedFeed.sendDonation() - } + .onTabAppear(.home) { + analytics.showedHome() + GoToFeedTip.viewedFeed.sendDonation() } .onChange(of: shouldNavigateToListsOnAppear) { if shouldNavigateToListsOnAppear { diff --git a/Nos/Views/Modifiers/OnTabAppearModifier.swift b/Nos/Views/Modifiers/OnTabAppearModifier.swift new file mode 100644 index 000000000..c4a7a0358 --- /dev/null +++ b/Nos/Views/Modifiers/OnTabAppearModifier.swift @@ -0,0 +1,46 @@ +import SwiftUI + +/// A view modifier that helps track when a tab becomes visible or invisible in a TabView. +struct OnTabAppearModifier: ViewModifier { + @EnvironmentObject private var router: Router + let tab: AppDestination + let onAppear: (() async -> Void)? + let onDisappear: (() async -> Void)? + + @State private var isVisible = false + + func body(content: Content) -> some View { + content + .onAppear { + if router.selectedTab == tab { + isVisible = true + } + } + .onDisappear { isVisible = false } + .onChange(of: isVisible) { + if isVisible { + Task { await onAppear?() } + } else { + Task { await onDisappear?() } + } + } + } +} + +extension View { + /// Executes an action when a specific tab becomes visible + /// - Parameters: + /// - tab: The tab to monitor for visibility + /// - action: The action to perform when the tab becomes visible + func onTabAppear(_ tab: AppDestination, perform action: @escaping () async -> Void) -> some View { + modifier(OnTabAppearModifier(tab: tab, onAppear: action, onDisappear: nil)) + } + + /// Executes an action when a specific tab is navigated away from + /// - Parameters: + /// - tab: The tab to monitor for visibility + /// - action: The action to perform when the tab becomes invisible + func onTabDisappear(_ tab: AppDestination, perform action: @escaping () async -> Void) -> some View { + modifier(OnTabAppearModifier(tab: tab, onAppear: nil, onDisappear: action)) + } +} diff --git a/Nos/Views/Notifications/NotificationsView.swift b/Nos/Views/Notifications/NotificationsView.swift index ce962c4a8..b0fcb147c 100644 --- a/Nos/Views/Notifications/NotificationsView.swift +++ b/Nos/Views/Notifications/NotificationsView.swift @@ -94,25 +94,15 @@ struct NotificationsView: View { .refreshable { await subscribeToNewEvents() } - .onAppear { - if router.selectedTab == .notifications { - isVisible = true - } + .onTabAppear(.notifications) { pushNotificationService.requestNotificationPermissionsFromUser() + analytics.showedNotifications() + await subscribeToNewEvents() + await markAllNotificationsRead() } - .onDisappear { - isVisible = false - } - .onChange(of: isVisible) { - Task { await markAllNotificationsRead() } - if isVisible { - analytics.showedNotifications() - Task { - await subscribeToNewEvents() - } - } else { - Task { await cancelSubscriptions() } - } + .onTabDisappear(.notifications) { + await cancelSubscriptions() + await markAllNotificationsRead() } .doubleTapToPop(tab: .notifications) { proxy in if let firstEvent = events.first { diff --git a/Nos/Views/Profile/ProfileTab.swift b/Nos/Views/Profile/ProfileTab.swift index 05ab71b46..35f495672 100644 --- a/Nos/Views/Profile/ProfileTab.swift +++ b/Nos/Views/Profile/ProfileTab.swift @@ -1,18 +1,22 @@ import Combine import SwiftUI +import Dependencies /// A version of the ProfileView that is displayed in the main tab bar struct ProfileTab: View { @Environment(CurrentUser.self) var currentUser + @EnvironmentObject private var router: Router + @Dependency(\.analytics) private var analytics @ObservedObject var author: Author - - @Binding var path: NavigationPath var body: some View { - NosNavigationStack(path: $path) { + NosNavigationStack(path: $router.profilePath) { ProfileView(author: author, addDoubleTapToPop: true) .navigationBarItems(leading: SideMenuButton()) + .onTabAppear(.profile) { + analytics.showedProfileTab() + } } } } diff --git a/Nos/Views/Profile/ProfileView.swift b/Nos/Views/Profile/ProfileView.swift index b12abf5a6..56b89a783 100644 --- a/Nos/Views/Profile/ProfileView.swift +++ b/Nos/Views/Profile/ProfileView.swift @@ -189,14 +189,8 @@ struct ProfileView: View { } ) .alert(unwrapping: $alert) - .onAppear { - Task { - await downloadAuthorData() - } - analytics.showedProfile() - } - .onDisappear { - relaySubscriptions.removeAll() + .task { + await downloadAuthorData() } }