Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better 'no posts to be found' message #714

Merged
merged 5 commits into from
Oct 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Mlem.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
03A1B3F42A83F46200AB0DE0 /* ShareButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A1B3F32A83F46200AB0DE0 /* ShareButtonView.swift */; };
03A1B3F72A84000400AB0DE0 /* APIContentAggregatesProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A1B3F62A84000400AB0DE0 /* APIContentAggregatesProtocol.swift */; };
03A1B3F92A8400DD00AB0DE0 /* APIContentViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A1B3F82A8400DD00AB0DE0 /* APIContentViewProtocol.swift */; };
03A40DAD2AD5EA11005F019F /* NoPostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A40DAC2AD5EA11005F019F /* NoPostsView.swift */; };
03B643572A6864CD00F65700 /* TabBarSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B643562A6864CD00F65700 /* TabBarSettingsView.swift */; };
03B7AAEF2ABCB9DC00068B23 /* ContentTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B7AAEE2ABCB9DC00068B23 /* ContentTracker.swift */; };
03B7AAF12ABE404300068B23 /* ContentModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B7AAF02ABE404300068B23 /* ContentModel.swift */; };
Expand Down Expand Up @@ -523,6 +524,7 @@
03A1B3F32A83F46200AB0DE0 /* ShareButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareButtonView.swift; sourceTree = "<group>"; };
03A1B3F62A84000400AB0DE0 /* APIContentAggregatesProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIContentAggregatesProtocol.swift; sourceTree = "<group>"; };
03A1B3F82A8400DD00AB0DE0 /* APIContentViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIContentViewProtocol.swift; sourceTree = "<group>"; };
03A40DAC2AD5EA11005F019F /* NoPostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoPostsView.swift; sourceTree = "<group>"; };
03B643562A6864CD00F65700 /* TabBarSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarSettingsView.swift; sourceTree = "<group>"; };
03B7AAEE2ABCB9DC00068B23 /* ContentTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTracker.swift; sourceTree = "<group>"; };
03B7AAF02ABE404300068B23 /* ContentModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1535,6 +1537,7 @@
children = (
632578172A29F83C00446A66 /* PostSortMenu.swift */,
63344C702A098060001BC616 /* Sidebar View.swift */,
03A40DAC2AD5EA11005F019F /* NoPostsView.swift */,
6D405B002A43E79400C65F9C /* Sidebar Header Avatar.swift */,
6D405B022A43E7DB00C65F9C /* Sidebar Header Label.swift */,
6D405B042A43E82300C65F9C /* Sidebar Header.swift */,
Expand Down Expand Up @@ -2984,6 +2987,7 @@
88B165B82A8643F4007C9115 /* View - NavigationBar Color.swift in Sources */,
030AC0522A64666C00037155 /* UserSettingsView.swift in Sources */,
CDA2C5262A705D6000649D5A /* PostEditor.swift in Sources */,
03A40DAD2AD5EA11005F019F /* NoPostsView.swift in Sources */,
6372184A2A3A2AAD008C4816 /* APIPost.swift in Sources */,
6D693A3E2A5113DF009E2D76 /* CreatePostReport.swift in Sources */,
CD391F942A533B7700E213B5 /* EditorModelProtocol.swift in Sources */,
Expand Down
12 changes: 10 additions & 2 deletions Mlem/Error Handling/ErrorHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ class ErrorHandler: ObservableObject {
handle(.init(underlyingError: error), file: file, function: function, line: line)
}

func handle(_ error: ContextualError?, file: StaticString = #fileID, function: StaticString = #function, line: Int = #line) {
func handle(
_ error: ContextualError?,
file: StaticString = #fileID,
function: StaticString = #function,
line: Int = #line,
showNoInternet: Bool = true
) {
guard let error else {
return
}
Expand All @@ -43,7 +49,9 @@ class ErrorHandler: ObservableObject {
}

if error.title != nil && !InternetConnectionManager.isConnectedToNetwork() {
await notifier.add(.noInternet)
if showNoInternet {
await notifier.add(.noInternet)
}
return
}

Expand Down
1 change: 1 addition & 0 deletions Mlem/Icons.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ struct Icons {
static let titleOnlyPost: String = "character.bubble"
static let pinned: String = "pin.fill"
static let websiteIcon: String = "globe"
static let hideRead: String = "book"

// post sizes
static let postSizeSetting: String = "rectangle.expand.vertical"
Expand Down
34 changes: 27 additions & 7 deletions Mlem/Models/Trackers/Post Tracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import Foundation
import Nuke
import SwiftUI

enum PostFilterReason {
case read, keyword
}
// swiftlint:disable type_body_length
// swiftlint:disable file_length
/// New post tracker built on top of the PostRepository instead of calling the API directly. Because this thing works fundamentally differently from the old one, it can't conform to FeedTracker--that's going to need a revamp down the line once everything uses nice shiny middleware models, so for now we're going to have to put up with some ugly
Expand All @@ -32,6 +35,7 @@ class PostTracker: ObservableObject {
private var ids: Set<ContentModelIdentifier> = .init(minimumCapacity: 1000)
private(set) var isLoading: Bool = false // accessible but not published because it causes lots of bad view redraws
private(set) var page: Int = 1
private(set) var hiddenItems: [PostFilterReason: Int] = .init()

private var hasReachedEnd: Bool = false

Expand Down Expand Up @@ -62,7 +66,7 @@ class PostTracker: ObservableObject {
communityId: Int?,
sort: PostSortType?,
type: FeedType,
filtering: @escaping (_: PostModel) -> Bool = { _ in true }
filtering: @escaping (_: PostModel) -> PostFilterReason? = { _ in nil }
) async throws {
let currentPage = page

Expand Down Expand Up @@ -99,7 +103,7 @@ class PostTracker: ObservableObject {
}

// don't preload filtered images
preloadImages(newPosts.filter(filtering))
preloadImages(filterItems(items: newPosts, with: filtering))
}

/// Loads a single post and adds it to the tracker
Expand All @@ -117,7 +121,7 @@ class PostTracker: ObservableObject {
sort: PostSortType?,
feedType: FeedType,
clearBeforeFetch: Bool = false,
filtering: @escaping (_: PostModel) -> Bool = { _ in true }
filtering: @escaping (_: PostModel) -> PostFilterReason? = { _ in nil }
) async throws {
if clearBeforeFetch {
await reset()
Expand All @@ -144,10 +148,10 @@ class PostTracker: ObservableObject {
/// - preload: true if the new post's image should be preloaded
func add(
_ newItems: [PostModel],
filtering: @escaping (_: PostModel) -> Bool = { _ in true },
filtering: @escaping (_: PostModel) -> PostFilterReason? = { _ in nil },
preload: Bool = false
) {
let accepted = dedupedItems(from: newItems.filter(filtering))
let accepted = dedupedItems(from: filterItems(items: newItems, with: filtering))

if preload { preloadImages(newItems) }

Expand All @@ -167,12 +171,15 @@ class PostTracker: ObservableObject {
@MainActor
func reset(
with newItems: [PostModel] = .init(),
filteredWith filter: @escaping (_: PostModel) -> Bool = { _ in true }
filteredWith filter: @escaping (_: PostModel) -> PostFilterReason? = { _ in nil }
) {
hasReachedEnd = false
page = newItems.isEmpty ? 1 : 2
if page == 1 {
hiddenItems.removeAll()
}
ids = .init(minimumCapacity: 1000)
items = dedupedItems(from: newItems.filter(filter))
items = dedupedItems(from: filterItems(items: newItems, with: filter))
}

/// Determines whether the tracker should load more items
Expand Down Expand Up @@ -458,6 +465,19 @@ class PostTracker: ObservableObject {
private func dedupedItems(from newItems: [PostModel]) -> [PostModel] {
newItems.filter { ids.insert($0.uid).inserted }
}

private func filterItems(
items: [PostModel],
with filtering: @escaping (_: PostModel) -> PostFilterReason? = { _ in nil }
) -> [PostModel] {
return items.filter { item in
if let reason = filtering(item) {
self.hiddenItems[reason] = self.hiddenItems[reason, default: 0] + 1
return false
}
return true
}
}
}

// swiftlint:enable type_body_length
Expand Down
8 changes: 7 additions & 1 deletion Mlem/Views/Shared/Loading View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ struct LoadingView: View {
let whatIsLoading: PossibleThingsToLoad

var body: some View {
VStack {
VStack(spacing: AppConstants.postAndCommentSpacing) {
Spacer()

ProgressView()
Expand Down Expand Up @@ -52,3 +52,9 @@ struct LoadingView: View {
.frame(maxWidth: .infinity)
}
}

struct LoadingViewPreview: PreviewProvider {
static var previews: some View {
LoadingView(whatIsLoading: .posts)
}
}
82 changes: 82 additions & 0 deletions Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//
// NoPostsView.swift
// Mlem
//
// Created by Sjmarf on 10/10/2023.
//

import SwiftUI

struct NoPostsView: View {
@EnvironmentObject var postTracker: PostTracker

@Binding var isLoading: Bool
@Binding var postSortType: PostSortType
@Binding var showReadPosts: Bool

var body: some View {
VStack {
if !isLoading {
VStack(alignment: .center, spacing: AppConstants.postAndCommentSpacing) {

let unreadItems = postTracker.hiddenItems[.read, default: 0]

Image(systemName: Icons.noPosts)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: unreadItems == 0 ? 35 : 50)
.padding(.bottom, unreadItems == 0 ? 8: 12)
Text(title)

if unreadItems != 0 {
Text(
"\(unreadItems) read post\(unreadItems == 1 ? " has" : "s have") been hidden."
)
.foregroundStyle(.tertiary)
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
.padding(.horizontal, 20)

}
buttons
}
.foregroundStyle(.secondary)
}
}
}

var title: String {
if PostSortType.topTypes.contains(postSortType) && postSortType != .topAll {
return "No posts found from the last \(postSortType.label.lowercased())."
}
return "No posts found."
}

@ViewBuilder
var buttons: some View {
VStack {
if postSortType != .hot {
Button {
isLoading = true
postSortType = .hot
} label: {
Label("Switch to Hot", systemImage: Icons.hotSort)
}
}
if postTracker.hiddenItems[.read, default: 0] > 0 {
Button {
if !showReadPosts {
isLoading = true
showReadPosts = true
}
} label: {
Text("Show read posts")
}
}
}
.foregroundStyle(.secondary)
.buttonStyle(.bordered)
.padding(.top)
.padding(.horizontal, 20)
}
}
33 changes: 16 additions & 17 deletions Mlem/Views/Tabs/Feeds/Feed View Logic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ extension FeedView {
}
}

func refreshFeed() async {
@discardableResult
func refreshFeed() async -> Bool {
// NOTE: refresh doesn't need to touch isLoading because that visual cue is handled by .refreshable
do {
try await postTracker.refresh(
Expand All @@ -45,8 +46,11 @@ extension FeedView {
feedType: feedType,
filtering: filter
)
errorDetails = nil
return true
} catch {
handle(error)
return false
}
}

Expand Down Expand Up @@ -79,7 +83,8 @@ extension FeedView {
title: "Could not load community information",
message: "The server might be overloaded.\nTry again later.",
underlyingError: error
)
),
showNoInternet: false
)
}
}
Expand Down Expand Up @@ -267,30 +272,24 @@ extension FeedView {
// MARK: Helper Functions

private func handle(_ error: Error) {
let title: String?
let errorMessage: String?


switch error {
case APIClientError.networking:
guard postTracker.items.isEmpty else {
return
}

title = "Unable to connect to Lemmy"
errorMessage = "Please check your internet connection and try again"
errorDetails = .init(title: "Unable to connect to Lemmy", error: error, refresh: self.refreshFeed)
return
default:
title = nil
errorMessage = nil
break
}

errorHandler.handle(
.init(title: title, message: errorMessage, underlyingError: error)
)
errorDetails = .init(error: error, refresh: self.refreshFeed)
}

private func filter(postView: PostModel) -> Bool {
!postView.post.name.lowercased().contains(filtersTracker.filteredKeywords) &&
(showReadPosts || !postView.read)
private func filter(postView: PostModel) -> PostFilterReason? {
guard !postView.post.name.lowercased().contains(filtersTracker.filteredKeywords) else { return .keyword }
guard showReadPosts || !postView.read else { return .read }
return nil
}

private func subscribe(communityId: Int, shouldSubscribe: Bool) async {
Expand Down
Loading
Loading