diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 32ade5526..d0a7e1e8a 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -93,6 +93,9 @@ 03531EF12C2DA298004A3464 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03531EF02C2DA298004A3464 /* SearchResultsView.swift */; }; 03531EF52C2DA610004A3464 /* NavigationSearchType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03531EF42C2DA610004A3464 /* NavigationSearchType.swift */; }; 035394862C9CDC1100795AA5 /* SubscriptionListNavigationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035394852C9CDC1100795AA5 /* SubscriptionListNavigationButton.swift */; }; + 0353948B2CA076D000795AA5 /* InboxView+Views.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0353948A2CA076D000795AA5 /* InboxView+Views.swift */; }; + 0353948D2CA080EB00795AA5 /* FeedWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0353948C2CA080EB00795AA5 /* FeedWelcomeView.swift */; }; + 0353948F2CA088E600795AA5 /* DeveloperSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0353948E2CA088E600795AA5 /* DeveloperSettingsView.swift */; }; 0355F9462C150B2300605248 /* ExternalApiInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0355F9452C150B2300605248 /* ExternalApiInfoView.swift */; }; 0355F9492C16406E00605248 /* Line.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0355F9482C16406E00605248 /* Line.swift */; }; 035BE0872BDD8DA000F77D73 /* NavigationRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035BE0862BDD8DA000F77D73 /* NavigationRootView.swift */; }; @@ -455,6 +458,9 @@ 03531EF02C2DA298004A3464 /* SearchResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = ""; }; 03531EF42C2DA610004A3464 /* NavigationSearchType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSearchType.swift; sourceTree = ""; }; 035394852C9CDC1100795AA5 /* SubscriptionListNavigationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionListNavigationButton.swift; sourceTree = ""; }; + 0353948A2CA076D000795AA5 /* InboxView+Views.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InboxView+Views.swift"; sourceTree = ""; }; + 0353948C2CA080EB00795AA5 /* FeedWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedWelcomeView.swift; sourceTree = ""; }; + 0353948E2CA088E600795AA5 /* DeveloperSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsView.swift; sourceTree = ""; }; 0355F9452C150B2300605248 /* ExternalApiInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalApiInfoView.swift; sourceTree = ""; }; 0355F9482C16406E00605248 /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = ""; }; 035BE0862BDD8DA000F77D73 /* NavigationRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRootView.swift; sourceTree = ""; }; @@ -775,6 +781,7 @@ 031E2D5C2BEFCC630003BC45 /* SettingsView.swift */, 039F58892C7B54FE00C61658 /* GeneralSettingsView.swift */, 039F588B2C7B574E00C61658 /* AdvancedSettingsView.swift */, + 0353948E2CA088E600795AA5 /* DeveloperSettingsView.swift */, 039F58962C7B68F100C61658 /* AboutMlemView.swift */, 0397D46B2C67E583002C6CDC /* SortingSettingsView.swift */, 03267D832BED49CE009D6268 /* AccountSettingsView.swift */, @@ -935,6 +942,7 @@ isa = PBXGroup; children = ( 0369B3552BFA6824001EFEDF /* InboxView.swift */, + 0353948A2CA076D000795AA5 /* InboxView+Views.swift */, 03B431BD2C455F1B001A1EB5 /* InboxView+Logic.swift */, ); path = Inbox; @@ -1517,6 +1525,7 @@ isa = PBXGroup; children = ( CD7928222C73CBA400FA712D /* TileScoreView.swift */, + 0353948C2CA080EB00795AA5 /* FeedWelcomeView.swift */, ); path = Components; sourceTree = ""; @@ -1952,6 +1961,7 @@ 0389DDCF2C39CB0E0005B808 /* SearchView.swift in Sources */, 0397D4742C680FFC002C6CDC /* ImageLoader.swift in Sources */, 03FE14042BF93FDD00A8377F /* ErrorDetails.swift in Sources */, + 0353948B2CA076D000795AA5 /* InboxView+Views.swift in Sources */, 0389DDD12C39E1030005B808 /* InstanceListRowBody.swift in Sources */, 033FCAF42C59843E007B7CD1 /* CommunityView.swift in Sources */, CD7928262C73E73400FA712D /* PersonView+Logic.swift in Sources */, @@ -2088,6 +2098,7 @@ 03D3A1F32BB9D49B009DE55E /* ActionGroup.swift in Sources */, 03049A222C650B2C00FF6889 /* CommunityDetailsView.swift in Sources */, CD7882A92BFFDFC7002E1A30 /* PostTag.swift in Sources */, + 0353948F2CA088E600795AA5 /* DeveloperSettingsView.swift in Sources */, CD7882AB2C013005002E1A30 /* EllipsisMenu.swift in Sources */, 039D75672C4FC5A3004F24C2 /* LargeImageView.swift in Sources */, CDBFCB6A2C04EFFE008CD468 /* PostSettingsView.swift in Sources */, @@ -2193,6 +2204,7 @@ 03A82FA32C0D1F2400D01A5C /* View+ExternalApiWarning.swift in Sources */, 033F84C32C2B12AA002E3EDF /* InstanceSummary.swift in Sources */, 0369B3532BFA514B001EFEDF /* ToastLocation.swift in Sources */, + 0353948D2CA080EB00795AA5 /* FeedWelcomeView.swift in Sources */, 03049A202C650A8100FF6889 /* FormReadout.swift in Sources */, 0397D49A2C6EA6EE002C6CDC /* InteractionBarEditorView.swift in Sources */, 0300309D2C4163C9009A65FF /* CommentTreeTracker.swift in Sources */, diff --git a/Mlem/App/Configuration/User Settings/Settings.swift b/Mlem/App/Configuration/User Settings/Settings.swift index 38e400cd9..b48af2d0d 100644 --- a/Mlem/App/Configuration/User Settings/Settings.swift +++ b/Mlem/App/Configuration/User Settings/Settings.swift @@ -67,6 +67,8 @@ class Settings: ObservableObject { @AppStorage("status.bypassImageProxyShown") var bypassImageProxyShown: Bool = false + @AppStorage("tip.feedWelcomePrompt") var showFeedWelcomePrompt: Bool = true + var codable: CodableSettings { .init(from: self) } @MainActor diff --git a/Mlem/App/Globals/Definitions/AccountsTracker.swift b/Mlem/App/Globals/Definitions/AccountsTracker.swift index 7bd1fe9f4..f274e0c73 100644 --- a/Mlem/App/Globals/Definitions/AccountsTracker.swift +++ b/Mlem/App/Globals/Definitions/AccountsTracker.swift @@ -32,6 +32,8 @@ class AccountsTracker { try! (guestAccounts.first ?? .getGuestAccount(url: URL(string: "https://lemmy.world/")!)) } + var isEmpty: Bool { userAccounts.isEmpty && guestAccounts.isEmpty } + private var cancellables = Set() private init() { diff --git a/Mlem/App/Views/Root/Tabs/Feeds/Components/FeedWelcomeView.swift b/Mlem/App/Views/Root/Tabs/Feeds/Components/FeedWelcomeView.swift new file mode 100644 index 000000000..2961c7600 --- /dev/null +++ b/Mlem/App/Views/Root/Tabs/Feeds/Components/FeedWelcomeView.swift @@ -0,0 +1,61 @@ +// +// FeedWelcomeView.swift +// Mlem +// +// Created by Sjmarf on 22/09/2024. +// + +import SwiftUI + +struct FeedWelcomeView: View { + @Environment(AppState.self) var appState + @Environment(Palette.self) var palette + @Environment(NavigationLayer.self) var navigation + + @Setting(\.showFeedWelcomePrompt) var showWelcomePrompt + + var body: some View { + VStack(spacing: Constants.main.standardSpacing) { + HStack(spacing: Constants.main.standardSpacing) { + VStack(alignment: .leading) { + Text("Welcome to Lemmy!") + .fontWeight(.semibold) + Text( + // swiftlint:disable:next line_length + "You are browsing \(appState.firstApi.host ?? "") as a guest. If you'd like to vote or reply, you'll need to log in or sign up." + ) + .font(.footnote) + } + } + .foregroundStyle(palette.accent) + HStack(spacing: Constants.main.standardSpacing) { + Button { + navigation.openSheet(.logIn(.pickInstance)) + } label: { + Text("Log In") + .frame(maxWidth: 400) + .padding(.vertical, 4) + } + Button { + navigation.openSheet(.signUp()) + } label: { + Text("Sign Up") + .frame(maxWidth: 400) + .padding(.vertical, 4) + } + } + .buttonStyle(.borderedProminent) + } + .padding(Constants.main.standardSpacing) + .background(palette.accent.opacity(0.2), in: .rect(cornerRadius: Constants.main.standardSpacing)) + .overlay(alignment: .topTrailing) { + Button("Dismiss", systemImage: Icons.closeCircleFill) { + showWelcomePrompt = false + } + .symbolRenderingMode(.hierarchical) + .labelStyle(.iconOnly) + .fontWeight(.semibold) + .padding(4) + } + } +} diff --git a/Mlem/App/Views/Root/Tabs/Feeds/Feed Header/FeedDescription.swift b/Mlem/App/Views/Root/Tabs/Feeds/Feed Header/FeedDescription.swift index 7347f9974..8c3950483 100644 --- a/Mlem/App/Views/Root/Tabs/Feeds/Feed Header/FeedDescription.swift +++ b/Mlem/App/Views/Root/Tabs/Feeds/Feed Header/FeedDescription.swift @@ -26,14 +26,22 @@ struct FeedDescription { iconScaleFactor: 0.6 ) - static var local: FeedDescription = .init( - label: "Local", - subtitle: "Posts from \(AppState.main.firstApi.host ?? "your instance's") communities", - color: { $0.localFeed }, - iconName: Icons.instanceFeed, - iconNameFill: Icons.instanceFeedFill, - iconScaleFactor: 0.55 - ) + static var local: FeedDescription { + let subtitle: LocalizedStringResource + if let host = AppState.main.firstApi.host { + subtitle = "Posts from \(host) communities" + } else { + subtitle = "Posts from your instance's communities" + } + return .init( + label: "Local", + subtitle: subtitle, + color: { $0.localFeed }, + iconName: Icons.instanceFeed, + iconNameFill: Icons.instanceFeedFill, + iconScaleFactor: 0.55 + ) + } static var subscribed: FeedDescription = .init( label: "Subscribed", diff --git a/Mlem/App/Views/Root/Tabs/Feeds/FeedsView.swift b/Mlem/App/Views/Root/Tabs/Feeds/FeedsView.swift index 5434132de..9e71fc96d 100644 --- a/Mlem/App/Views/Root/Tabs/Feeds/FeedsView.swift +++ b/Mlem/App/Views/Root/Tabs/Feeds/FeedsView.swift @@ -13,6 +13,7 @@ import SwiftUI struct FeedsView: View { @Setting(\.postSize) var postSize @Setting(\.showReadInFeed) var showRead + @Setting(\.showFeedWelcomePrompt) var showWelcomePrompt @Environment(AppState.self) var appState @Environment(Palette.self) var palette @@ -147,6 +148,10 @@ struct FeedsView: View { var content: some View { FancyScrollView(scrollToTopTrigger: $scrollToTopTrigger) { Section { + if AccountsTracker.main.isEmpty, showWelcomePrompt { + FeedWelcomeView() + .padding([.horizontal, .bottom], Constants.main.standardSpacing) + } if let savedFeedLoader, feedSelection == .saved { PersonContentGridView(feedLoader: savedFeedLoader, contentType: .constant(.all)) } else if let postFeedLoader { diff --git a/Mlem/App/Views/Root/Tabs/Feeds/SubscriptionListView.swift b/Mlem/App/Views/Root/Tabs/Feeds/SubscriptionListView.swift index 7812714c7..03ce93d9d 100644 --- a/Mlem/App/Views/Root/Tabs/Feeds/SubscriptionListView.swift +++ b/Mlem/App/Views/Root/Tabs/Feeds/SubscriptionListView.swift @@ -38,7 +38,6 @@ struct SubscriptionListView: View { @ViewBuilder var content: some View { - let subscriptions = (appState.firstSession as? UserSession)?.subscriptions let sections = subscriptions?.visibleSections(sort: sort) ?? [] ScrollViewReader { proxy in @@ -47,8 +46,18 @@ struct SubscriptionListView: View { ForEach(feedOptions, id: \.hashValue) { feedOption in SubscriptionListNavigationButton(.feeds(feedOption)) { HStack(spacing: 15) { - FeedIconView(feedDescription: feedOption.description, size: 28) - Text(feedOption.description.label) + FeedIconView( + feedDescription: feedOption.description, + size: appState.firstSession is GuestSession ? 36 : 28 + ) + VStack(alignment: .leading) { + Text(feedOption.description.label) + if appState.firstSession is GuestSession { + Text(feedOption.description.subtitle) + .font(.footnote) + .foregroundStyle(palette.secondary) + } + } } .frame(maxWidth: .infinity, alignment: .leading) .contentShape(.rect) @@ -56,6 +65,13 @@ struct SubscriptionListView: View { } } + if AccountsTracker.main.isEmpty { + Section { + signedOutInfoView + .listRowBackground(Color.clear) + } + } + ForEach(sections) { section in SubscriptionListSectionView(section: section, sectionIndicesShown: sectionIndicesShown) .id(section.label) @@ -73,9 +89,11 @@ struct SubscriptionListView: View { } } .toolbar { - Picker("Sort", selection: $sort) { - ForEach(SubscriptionListSort.allCases, id: \.self) { item in - Label(item.label, systemImage: item.systemImage) + if !(subscriptions?.communities.isEmpty ?? true) { + Picker("Sort", selection: $sort) { + ForEach(SubscriptionListSort.allCases, id: \.self) { item in + Label(item.label, systemImage: item.systemImage) + } } } } @@ -100,8 +118,47 @@ struct SubscriptionListView: View { } } + @ViewBuilder + var signedOutInfoView: some View { + VStack { + Image(systemName: "list.bullet") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 50) + .foregroundStyle(palette.tertiary) + .padding(.bottom, 5) + Text("Your subscriptions live here.") + .font(.title2) + .fontWeight(.semibold) + Text("Log in or sign up to view your subscriptions.") + HStack { + Button { + navigation.openSheet(.logIn(.pickInstance)) + } label: { + Text("Log In") + .frame(minWidth: 80) + } + Button { + navigation.openSheet(.signUp()) + } label: { + Text("Sign Up") + .frame(minWidth: 80) + } + } + .tint(palette.secondary) + .buttonStyle(.bordered) + } + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .foregroundColor(palette.secondary) + } + + var subscriptions: SubscriptionList? { + (appState.firstSession as? UserSession)?.subscriptions + } + var sectionIndicesShown: Bool { - !UIDevice.isPad && sort == .alphabetical + !UIDevice.isPad && sort == .alphabetical && (subscriptions?.communities.count ?? 0) > 10 } } diff --git a/Mlem/App/Views/Root/Tabs/Inbox/InboxView+Views.swift b/Mlem/App/Views/Root/Tabs/Inbox/InboxView+Views.swift new file mode 100644 index 000000000..63e484d29 --- /dev/null +++ b/Mlem/App/Views/Root/Tabs/Inbox/InboxView+Views.swift @@ -0,0 +1,167 @@ +// +// InboxView+Views.swift +// Mlem +// +// Created by Sjmarf on 22/09/2024. +// + +import SwiftUI + +extension InboxView { + @ViewBuilder + var sectionHeader: some View { + BubblePicker( + Tab.allCases, + selected: $selectedTab, + label: \.label, + value: { tab in + if let unreadCount = (appState.firstSession as? UserSession)?.unreadCount { + switch tab { + case .all: + return unreadCount.total + case .replies: + return unreadCount.replies + case .mentions: + return unreadCount.mentions + case .messages: + return unreadCount.messages + } + } + return 0 + } + ) + .background(palette.groupedBackground.opacity(headerPinned ? 1 : 0)) + .background(.bar) + } + + @ViewBuilder + var refreshPopup: some View { + HStack(spacing: 0) { + Text("Inbox is outdated") + .padding(.horizontal, 10) + Button { + showRefreshPopup = false + HapticManager.main.play(haptic: .lightSuccess, priority: .high) + Task { @MainActor in + removeAll() + await loadReplies() + } + } label: { + Label("Refresh", systemImage: Icons.refresh) + .foregroundStyle(palette.selectedInteractionBarItem) + .fontWeight(.semibold) + .padding(.vertical, 4) + .padding(.horizontal, 10) + .background(palette.accent, in: .capsule) + } + .buttonStyle(.plain) + } + .padding(4) + .background(palette.secondaryBackground, in: .capsule) + .shadow(color: .black.opacity(0.1), radius: 5) + .shadow(color: .black.opacity(0.1), radius: 1) + .padding() + } + + @ToolbarContentBuilder + var toolbar: some ToolbarContent { + ToolbarItem(placement: .topBarLeading) { + Button { + showRead.toggle() + } label: { + Label("Hide Read", systemImage: Icons.filter) + .symbolVariant(showRead ? .none : .fill) + } + } + ToolbarItem(placement: .topBarTrailing) { + markAllAsReadButton + } + } + + @ViewBuilder + var markAllAsReadButton: some View { + let newMessagesExist = !waitingOnMarkAllAsRead && ((appState.firstSession as? UserSession)?.unreadCount?.total ?? 0) != 0 + PhaseAnimator([0, 1], trigger: markAllAsReadTrigger) { value in + Button { + HapticManager.main.play(haptic: .gentleInfo, priority: .low) + waitingOnMarkAllAsRead = true + markAllAsReadTrigger.toggle() + Task { + do { + try await appState.firstApi.markAllAsRead() + if !showRead { + removeAll() + } + try await Task.sleep(for: .seconds(0.05)) + } catch { + handleError(error) + } + waitingOnMarkAllAsRead = false + } + } label: { + HStack { + Image(systemName: Icons.markRead) + .imageScale(.small) + Text("All") + } + .opacity((value == 0 && newMessagesExist) ? 1 : 0) + .overlay { + if value != 0 { + Image(systemName: Icons.success) + .imageScale(.small) + .fontWeight(.semibold) + } + } + .fixedSize() + .padding(.vertical, 2) + .padding(.horizontal, 10) + .background(.bar, in: .capsule) + } + .opacity((newMessagesExist || value != 0) ? 1 : 0) + } + } + + @ViewBuilder + var signedOutInfoView: some View { + VStack { + Image(systemName: Icons.inbox) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 80) + .foregroundColor(palette.accent) + Text(AccountsTracker.main.isEmpty ? "Log in or sign up to view your inbox." : "Switch account to view your inbox.") + .font(.title2) + .padding(.horizontal) + .fontWeight(.semibold) + .padding(.bottom, Constants.main.halfSpacing) + if AccountsTracker.main.isEmpty { + HStack { + infoViewButton("Log In") { + navigation.openSheet(.logIn(.pickInstance)) + } + infoViewButton("Sign Up") { + navigation.openSheet(.signUp()) + } + } + } else { + infoViewButton("Switch Account") { + navigation.openSheet(.quickSwitcher) + } + } + } + .buttonStyle(.borderedProminent) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding(.horizontal) + } + + @ViewBuilder + private func infoViewButton(_ title: LocalizedStringResource, callback: @escaping () -> Void) -> some View { + Button(action: callback) { + Text(title) + .padding(.vertical, 4) + .padding(.horizontal, 8) + .frame(minWidth: 100) + } + } +} diff --git a/Mlem/App/Views/Root/Tabs/Inbox/InboxView.swift b/Mlem/App/Views/Root/Tabs/Inbox/InboxView.swift index 8450911c7..5a6a9d855 100644 --- a/Mlem/App/Views/Root/Tabs/Inbox/InboxView.swift +++ b/Mlem/App/Views/Root/Tabs/Inbox/InboxView.swift @@ -60,44 +60,48 @@ struct InboxView: View { } var body: some View { - content - .background(palette.groupedBackground) - .navigationBarTitleDisplayMode(.inline) - .toolbar { toolbar } - .onChange(of: taskId) { - Task { @MainActor in - showRefreshPopup = false - removeAll() - await loadReplies() - } - } - .refreshable { - _ = await Task { - await loadReplies() - }.result - } - .onAppear { - guard !hasDoneInitialLoad else { return } - hasDoneInitialLoad = true - Task { @MainActor in - await loadReplies() + if appState.firstSession is GuestSession { + signedOutInfoView + } else { + content + .background(palette.groupedBackground) + .navigationBarTitleDisplayMode(.inline) + .toolbar { toolbar } + .onChange(of: taskId) { + Task { @MainActor in + showRefreshPopup = false + removeAll() + await loadReplies() + } } - } - .onChange(of: (appState.firstSession as? UserSession)?.unreadCount?.updateId ?? 0) { oldValue, newValue in - // The newValue > oldValue check stops the popup from appearing when the user switches accounts. - // This is a little janky, but it works - if newValue > oldValue, loadingState == .done { - showRefreshPopup = true + .refreshable { + _ = await Task { + await loadReplies() + }.result } - } - .overlay(alignment: .bottom) { - RefreshPopupView("Inbox is outdated", isPresented: $showRefreshPopup) { + .onAppear { + guard !hasDoneInitialLoad else { return } + hasDoneInitialLoad = true Task { @MainActor in - removeAll() await loadReplies() } } - } + .onChange(of: (appState.firstSession as? UserSession)?.unreadCount?.updateId ?? 0) { oldValue, newValue in + // The newValue > oldValue check stops the popup from appearing when the user switches accounts. + // This is a little janky, but it works + if newValue > oldValue, loadingState == .done { + showRefreshPopup = true + } + } + .overlay(alignment: .bottom) { + RefreshPopupView("Inbox is outdated", isPresented: $showRefreshPopup) { + Task { @MainActor in + removeAll() + await loadReplies() + } + } + } + } } @ViewBuilder @@ -159,119 +163,6 @@ struct InboxView: View { } .coordinateSpace(name: "inboxScrollView") } - - @ViewBuilder - var sectionHeader: some View { - BubblePicker( - Tab.allCases, - selected: $selectedTab, - label: \.label, - value: { tab in - if let unreadCount = (appState.firstSession as? UserSession)?.unreadCount { - switch tab { - case .all: - return unreadCount.total - case .replies: - return unreadCount.replies - case .mentions: - return unreadCount.mentions - case .messages: - return unreadCount.messages - } - } - return 0 - } - ) - .background(palette.groupedBackground.opacity(headerPinned ? 1 : 0)) - .background(.bar) - } - - @ViewBuilder - var refreshPopup: some View { - HStack(spacing: 0) { - Text("Inbox is outdated") - .padding(.horizontal, 10) - Button { - showRefreshPopup = false - HapticManager.main.play(haptic: .lightSuccess, priority: .high) - Task { @MainActor in - removeAll() - await loadReplies() - } - } label: { - Label("Refresh", systemImage: Icons.refresh) - .foregroundStyle(palette.selectedInteractionBarItem) - .fontWeight(.semibold) - .padding(.vertical, 4) - .padding(.horizontal, 10) - .background(palette.accent, in: .capsule) - } - .buttonStyle(.plain) - } - .padding(4) - .background(palette.secondaryBackground, in: .capsule) - .shadow(color: .black.opacity(0.1), radius: 5) - .shadow(color: .black.opacity(0.1), radius: 1) - .padding() - } - - @ToolbarContentBuilder - var toolbar: some ToolbarContent { - ToolbarItem(placement: .topBarLeading) { - Button { - showRead.toggle() - } label: { - Label("Hide Read", systemImage: Icons.filter) - .symbolVariant(showRead ? .none : .fill) - } - } - ToolbarItem(placement: .topBarTrailing) { - markAllAsReadButton - } - } - - @ViewBuilder - var markAllAsReadButton: some View { - let newMessagesExist = !waitingOnMarkAllAsRead && ((appState.firstSession as? UserSession)?.unreadCount?.total ?? 0) != 0 - PhaseAnimator([0, 1], trigger: markAllAsReadTrigger) { value in - Button { - HapticManager.main.play(haptic: .gentleInfo, priority: .low) - waitingOnMarkAllAsRead = true - markAllAsReadTrigger.toggle() - Task { - do { - try await appState.firstApi.markAllAsRead() - if !showRead { - removeAll() - } - try await Task.sleep(for: .seconds(0.05)) - } catch { - handleError(error) - } - waitingOnMarkAllAsRead = false - } - } label: { - HStack { - Image(systemName: Icons.markRead) - .imageScale(.small) - Text("All") - } - .opacity((value == 0 && newMessagesExist) ? 1 : 0) - .overlay { - if value != 0 { - Image(systemName: Icons.success) - .imageScale(.small) - .fontWeight(.semibold) - } - } - .fixedSize() - .padding(.vertical, 2) - .padding(.horizontal, 10) - .background(.bar, in: .capsule) - } - .opacity((newMessagesExist || value != 0) ? 1 : 0) - } - } } private struct ScrollOffsetKey: PreferenceKey { diff --git a/Mlem/App/Views/Root/Tabs/Settings/AccountSettingsView.swift b/Mlem/App/Views/Root/Tabs/Settings/AccountSettingsView.swift index 12aecd56f..f5c8a7a1a 100644 --- a/Mlem/App/Views/Root/Tabs/Settings/AccountSettingsView.swift +++ b/Mlem/App/Views/Root/Tabs/Settings/AccountSettingsView.swift @@ -27,6 +27,12 @@ struct AccountSettingsView: View { .padding(.horizontal, -16) } + Section { + Button("Sign Out") { + appState.firstAccount.signOut() + } + } + if let account = appState.firstAccount as? UserAccount { Section { Button("Delete Account", role: .destructive) { diff --git a/Mlem/App/Views/Root/Tabs/Settings/AdvancedSettingsView.swift b/Mlem/App/Views/Root/Tabs/Settings/AdvancedSettingsView.swift index dc0439b9d..13504e78f 100644 --- a/Mlem/App/Views/Root/Tabs/Settings/AdvancedSettingsView.swift +++ b/Mlem/App/Views/Root/Tabs/Settings/AdvancedSettingsView.swift @@ -32,6 +32,9 @@ struct AdvancedSettingsView: View { URLCache.shared.removeAllCachedResponses() ImagePipeline.shared.cache.removeAll() } + Section { + NavigationLink("Developer", destination: .settings(.developer)) + } } .navigationTitle("Advanced") } diff --git a/Mlem/App/Views/Root/Tabs/Settings/DeveloperSettingsView.swift b/Mlem/App/Views/Root/Tabs/Settings/DeveloperSettingsView.swift new file mode 100644 index 000000000..45ce3ce3f --- /dev/null +++ b/Mlem/App/Views/Root/Tabs/Settings/DeveloperSettingsView.swift @@ -0,0 +1,24 @@ +// +// DeveloperSettingsView.swift +// Mlem +// +// Created by Sjmarf on 22/09/2024. +// + +import SwiftUI + +// Strings in this view are intentionally left unlocalized; we shouldn't +// be burdening translators with these when they'll never be used + +struct DeveloperSettingsView: View { + @Setting(\.showFeedWelcomePrompt) var showFeedWelcomePrompt + + var body: some View { + Form { + Button(String("Reset Feed Welcome Prompt")) { + showFeedWelcomePrompt = true + } + } + .navigationTitle("Developer") + } +} diff --git a/Mlem/App/Views/Shared/Accounts/AccountListView.swift b/Mlem/App/Views/Shared/Accounts/AccountListView.swift index 71ef7e901..f7f553b5a 100644 --- a/Mlem/App/Views/Shared/Accounts/AccountListView.swift +++ b/Mlem/App/Views/Shared/Accounts/AccountListView.swift @@ -106,11 +106,7 @@ struct AccountListView: View { navigation.openSheet(.logIn()) } Button("Sign Up") { - navigation.openSheet(.instancePicker(callback: { instance, navigation in - if let stub = instance.instanceStub { - navigation.push(.signUp(stub)) - } - })) + navigation.openSheet(.signUp()) } Button("Add Guest") { navigation.openSheet(.instancePicker(callback: { instance in diff --git a/Mlem/App/Views/Shared/Navigation/NavigationPage.swift b/Mlem/App/Views/Shared/Navigation/NavigationPage.swift index 653c2b4c5..ecb2486a1 100644 --- a/Mlem/App/Views/Shared/Navigation/NavigationPage.swift +++ b/Mlem/App/Views/Shared/Navigation/NavigationPage.swift @@ -105,6 +105,14 @@ enum NavigationPage: Hashable { return instancePicker(callback: .init(wrappedValue: callback), minimumVersion: minimumVersion) } + static func signUp() -> NavigationPage { + .instancePicker(callback: { instance, navigation in + if let stub = instance.instanceStub { + navigation.push(.signUp(stub)) + } + }) + } + static func communityPicker( api: ApiClient? = nil, callback: @escaping (Community2) -> Void diff --git a/Mlem/App/Views/Shared/Navigation/SettingsPage.swift b/Mlem/App/Views/Shared/Navigation/SettingsPage.swift index 485261804..6079e9f23 100644 --- a/Mlem/App/Views/Shared/Navigation/SettingsPage.swift +++ b/Mlem/App/Views/Shared/Navigation/SettingsPage.swift @@ -15,7 +15,7 @@ enum SettingsPage: Hashable { case importExportSettings case theme, icon case post, comment, inbox, subscriptionList - case about, advanced + case about, advanced, developer case postInteractionBar, commentInteractionBar, replyInteractionBar case licences, document(Document) @@ -35,6 +35,8 @@ enum SettingsPage: Hashable { ImportExportSettingsPage() case .advanced: AdvancedSettingsView() + case .developer: + DeveloperSettingsView() case .about: AboutMlemView() case .theme: diff --git a/Mlem/Localizable.xcstrings b/Mlem/Localizable.xcstrings index 5c6578a12..46fbe27b7 100644 --- a/Mlem/Localizable.xcstrings +++ b/Mlem/Localizable.xcstrings @@ -345,6 +345,9 @@ }, "Details" : { + }, + "Developer" : { + }, "Diagnosing..." : { @@ -632,6 +635,12 @@ }, "Log In" : { + }, + "Log in or sign up to view your inbox." : { + + }, + "Log in or sign up to view your subscriptions." : { + }, "Mark Read" : { @@ -827,6 +836,9 @@ }, "Posts from communities you subscribe to" : { + }, + "Posts from your instance's communities" : { + }, "Pride" : { @@ -998,6 +1010,9 @@ }, "Sign In to Lemmy" : { + }, + "Sign Out" : { + }, "Sign Up" : { @@ -1083,6 +1098,9 @@ }, "Switch Account" : { + }, + "Switch account to view your inbox." : { + }, "Switch to this account and..." : { @@ -1282,6 +1300,9 @@ }, "Welcome to %@" : { "comment" : "Example: \"Welcome to lemmy.world\"" + }, + "Welcome to Lemmy!" : { + }, "What is Federation?" : { @@ -1294,6 +1315,9 @@ }, "Yes (%@)" : { + }, + "You are browsing %@ as a guest. If you'd like to vote or reply, you'll need to log in or sign up." : { + }, "You are required to provide an email on this instance." : { @@ -1338,6 +1362,9 @@ }, "Your session has expired. Enter your password to authenticate a new session." : { + }, + "Your subscriptions live here." : { + } }, "version" : "1.0"