Merge pull request #3297 from omnivore-app/feat/ios-empty-following

feat/ios empty following
This commit is contained in:
Jackson Harper
2024-01-02 18:26:12 +08:00
committed by GitHub
14 changed files with 256 additions and 44 deletions

View File

@ -0,0 +1,70 @@
// swiftlint:disable line_length
import Foundation
import Models
import Services
import SwiftUI
import Views
public struct FollowingViewModal: View {
@Environment(\.dismiss) private var dismiss
let message: String = """
We've created a new place for all your newsletters and feeds called Following. You can control the destination of
new items by changing the destination for your subscriptions in the Subscriptions view of your settings. By default
your existing newsletters will go into your library and your existing feeds will go into Following.
From the library you can swipe items left to right to move them into your library. In the reader view you can tap the
bookmark icon on the toolbar to move items into your library.
If you don't need the following tab you can disable it from the filters view in your settings.
- [Learn more about the following](https://docs.omnivore.app/using/following.html)
- [Tell your friends about Omnivore](https://omnivore.app/about)
"""
var closeButton: some View {
Button(action: {
dismiss()
}, label: {
ZStack {
Circle()
.foregroundColor(Color.circleButtonBackground)
.frame(width: 30, height: 30)
Image(systemName: "xmark")
.resizable(resizingMode: Image.ResizingMode.stretch)
.foregroundColor(Color.circleButtonForeground)
.aspectRatio(contentMode: .fit)
.font(Font.title.weight(.bold))
.frame(width: 12, height: 12)
}
})
}
public var body: some View {
HStack {
Text("Your new Following tab")
.font(Font.system(size: 20, weight: .bold))
Spacer()
closeButton
}
.padding(.top, 16)
.padding(.horizontal, 16)
List {
Section {
let parsedMessage = try? AttributedString(markdown: message,
options: .init(interpretedSyntax: .inlineOnly))
Text(parsedMessage ?? "")
.multilineTextAlignment(.leading)
.foregroundColor(Color.appGrayTextContrast)
.accentColor(.blue)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, 16)
}
}
}
}

View File

@ -79,30 +79,73 @@ struct FiltersHeader: View {
struct EmptyState: View {
@ObservedObject var viewModel: HomeFeedViewModel
@EnvironmentObject var dataService: DataService
@State var showSendNewslettersAlert = false
var followingEmptyState: some View {
VStack(alignment: .center, spacing: 20) {
if viewModel.stopUsingFollowingPrimer {
VStack(spacing: 10) {
Image.relaxedSlothLight
Text("You are all caught up.").foregroundColor(Color.extensionTextSubtle)
Button(action: {
Task {
await viewModel.loadItems(dataService: dataService, isRefresh: true, loadingBarStyle: .simple)
}
}, label: { Text("Refresh").bold() })
.foregroundColor(Color.blue)
}
} else {
Text("You don't have any Feed items.")
.font(Font.system(size: 18, weight: .bold))
Text("Add an RSS/Atom feed")
.foregroundColor(Color.blue)
.onTapGesture {
viewModel.showAddFeedView = true
}
Text("Send your newsletters to following")
.foregroundColor(Color.blue)
.onTapGesture {
showSendNewslettersAlert = true
}
Text("Hide the Following tab")
.foregroundColor(Color.blue)
.onTapGesture {
viewModel.showHideFollowingAlert = true
}
}
}
.frame(minHeight: 400)
.frame(maxWidth: .infinity)
.padding()
.alert("Update newsletter destination", isPresented: $showSendNewslettersAlert, actions: {
Button(action: {
Task {
await viewModel.modifyingNewsletterDestinationToFollowing(dataService: dataService)
}
}, label: { Text("OK") })
Button(LocalText.cancelGeneric, role: .cancel) { showSendNewslettersAlert = false }
}, message: {
// swiftlint:disable:next line_length
Text("Your email address destination folders will be modified to send to this tab.\n\nAll new newsletters will appear here. You can modify the destination for each individual email address and subscription in your settings.")
})
}
var body: some View {
if viewModel.currentFolder == "following" {
if viewModel.isModifyingNewsletterDestination {
return AnyView(
VStack(alignment: .center, spacing: 20) {
Text("You don't have any Feed items.")
.font(Font.system(size: 18, weight: .bold))
Text("Add an RSS/Atom feed")
.foregroundColor(Color.blue)
.onTapGesture {
viewModel.showAddFeedView = true
}
Text("Hide the Following tab")
.foregroundColor(Color.blue)
.onTapGesture {
viewModel.showHideFollowingAlert = true
}
}
.frame(minHeight: 400)
.frame(maxWidth: .infinity)
.padding()
VStack {
Text("Modifying newsletter destinations...")
ProgressView()
}.frame(maxWidth: .infinity, maxHeight: .infinity)
)
} else if viewModel.currentFolder == "following" {
return AnyView(followingEmptyState)
} else {
return AnyView(Group {
Spacer()
@ -143,7 +186,6 @@ struct AnimatingCellHeight: AnimatableModifier {
@State var hasHighlightMutations = false
@State var searchPresented = false
@State var showAddLinkView = false
@State var showAddFeedView = false
@State var isListScrolled = false
@State var listTitle = ""
@State var isEditMode: EditMode = .inactive
@ -184,7 +226,6 @@ struct AnimatingCellHeight: AnimatableModifier {
isListScrolled: $isListScrolled,
prefersListLayout: $prefersListLayout,
isEditMode: $isEditMode,
showAddFeedView: $showAddFeedView,
selection: $selection,
viewModel: viewModel,
showFeatureCards: showFeatureCards
@ -240,10 +281,10 @@ struct AnimatingCellHeight: AnimatableModifier {
.sheet(item: $viewModel.itemForHighlightsView) { item in
NotebookView(viewModel: NotebookViewModel(item: item), hasHighlightMutations: $hasHighlightMutations)
}
.sheet(isPresented: $showAddFeedView) {
.sheet(isPresented: $viewModel.showAddFeedView) {
NavigationView {
LibraryAddFeedView(dismiss: {
showAddFeedView = false
viewModel.showAddFeedView = false
}, toastOperationHandler: nil)
}
}
@ -295,6 +336,11 @@ struct AnimatingCellHeight: AnimatableModifier {
if viewModel.appliedFilter == nil {
viewModel.setDefaultFilter()
}
// Once the user has seen at least one following item we stop displaying the
// initial help view
if viewModel.currentFolder == "following", viewModel.fetcher.items.count > 0 {
viewModel.stopUsingFollowingPrimer = true
}
}
.environment(\.editMode, self.$isEditMode)
.navigationBarTitleDisplayMode(.inline)
@ -346,7 +392,7 @@ struct AnimatingCellHeight: AnimatableModifier {
if viewModel.currentFolder == "inbox" {
showAddLinkView = true
} else if viewModel.currentFolder == "following" {
showAddFeedView = true
viewModel.showAddFeedView = true
}
},
label: {
@ -405,7 +451,6 @@ struct AnimatingCellHeight: AnimatableModifier {
@Binding var isListScrolled: Bool
@Binding var prefersListLayout: Bool
@Binding var isEditMode: EditMode
@Binding var showAddFeedView: Bool
@Binding var selection: Set<String>
@ObservedObject var viewModel: HomeFeedViewModel
@ -745,8 +790,16 @@ struct AnimatingCellHeight: AnimatableModifier {
}
}
if viewModel.showLoadingBar {
if viewModel.showLoadingBar == .redacted {
redactedItems
} else if viewModel.showLoadingBar == .simple {
VStack {
ProgressView()
}
.frame(minHeight: 400)
.frame(maxWidth: .infinity)
.padding()
.listRowSeparator(.hidden, edges: .all)
} else if viewModel.fetcher.items.isEmpty {
EmptyState(viewModel: viewModel)
.listRowSeparator(.hidden, edges: .all)
@ -906,13 +959,21 @@ struct AnimatingCellHeight: AnimatableModifier {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 325, maximum: 400), spacing: 16)], alignment: .center, spacing: 30) {
if viewModel.showLoadingBar {
if viewModel.showLoadingBar == .redacted {
ForEach(fakeLibraryItems(dataService: dataService), id: \.id) { item in
GridCard(item: item)
.aspectRatio(1.0, contentMode: .fill)
.background(Color.systemBackground)
.cornerRadius(6)
}.redacted(reason: .placeholder)
} else if viewModel.showLoadingBar == .simple {
VStack {
ProgressView()
}
.frame(minHeight: 400)
.frame(maxWidth: .infinity)
.padding()
.listRowSeparator(.hidden, edges: .all)
} else {
if !viewModel.fetcher.items.isEmpty {
ForEach(Array(viewModel.fetcher.items.enumerated()), id: \.1.id) { idx, item in

View File

@ -5,6 +5,12 @@ import SwiftUI
import Utils
import Views
enum LoadingBarStyle {
case none
case redacted
case simple
}
@MainActor final class HomeFeedViewModel: NSObject, ObservableObject {
let filterKey: String
@ObservedObject var fetcher: LibraryItemFetcher
@ -16,7 +22,7 @@ import Views
@Published var itemForHighlightsView: Models.LibraryItem?
@Published var linkRequest: LinkRequest?
@Published var presentWebContainer = false
@Published var showLoadingBar = true
@Published var showLoadingBar = LoadingBarStyle.redacted
@Published var selectedItem: Models.LibraryItem?
@Published var linkIsActive = false
@ -37,7 +43,10 @@ import Views
@State var lastMoreFetched: Date?
@State var lastFiltersFetched: Date?
@State var isModifyingNewsletterDestination = false
@AppStorage(UserDefaultKey.hideFeatureSection.rawValue) var hideFeatureSection = false
@AppStorage(UserDefaultKey.stopUsingFollowingPrimer.rawValue) var stopUsingFollowingPrimer = false
@AppStorage("LibraryTabView::hideFollowingTab") var hideFollowingTab = false
@Published var appliedFilter: InternalFilter? {
@ -191,9 +200,9 @@ import Views
}
}
func loadItems(dataService: DataService, isRefresh: Bool, forceRemote: Bool = false) async {
func loadItems(dataService: DataService, isRefresh: Bool, forceRemote: Bool = false, loadingBarStyle: LoadingBarStyle? = nil) async {
isLoading = true
showLoadingBar = isRefresh
showLoadingBar = isRefresh ? loadingBarStyle ?? .redacted : .none
if let filterState = filterState {
await fetcher.loadItems(
@ -205,7 +214,7 @@ import Views
}
isLoading = false
showLoadingBar = false
showLoadingBar = .none
}
func loadFeatureItems(context: NSManagedObjectContext, predicate: NSPredicate, sort: NSSortDescriptor) async -> [Models.LibraryItem] {
@ -325,4 +334,34 @@ import Views
func findFilter(_: DataService, named: String) -> InternalFilter? {
filters.first(where: { $0.name == named })
}
func modifyingNewsletterDestinationToFollowing(dataService: DataService) async {
isModifyingNewsletterDestination = true
do {
var errorCount = 0
let objectIDs = try await dataService.newsletterEmails()
let newsletters = await dataService.viewContext.perform {
let newsletters = objectIDs.compactMap { dataService.viewContext.object(with: $0) as? NewsletterEmail }
return newsletters
}
for newsletter in newsletters {
if let emailId = newsletter.emailId, newsletter.folder != "following" {
do {
try await dataService.updateNewsletterEmail(emailID: emailId, folder: "following")
} catch {
print("error updating newsletter: ", error)
errorCount += 1
}
}
}
if errorCount > 0 {
snackbar("There was an error modifying \(errorCount) of your emails")
} else {
snackbar("Email destination modified")
}
} catch {
snackbar("Error modifying emails")
}
}
}

View File

@ -21,9 +21,6 @@ struct LibraryTabView: View {
@AppStorage("LibraryTabView::hideFollowingTab") var hideFollowingTab = false
@AppStorage(UserDefaultKey.lastSelectedTabItem.rawValue) var selectedTab = "inbox"
@AppStorage(UserDefaultKey.followingPrimerDisplayed.rawValue) var followingPrimerDisplayed = false
@State var showFollowingPrimer = false
@State var showExpandedAudioPlayer = false
private let syncManager = LibrarySyncManager()
@ -74,14 +71,6 @@ struct LibraryTabView: View {
var body: some View {
VStack(spacing: 0) {
if showFollowingPrimer {
PresentationLink(transition: UIDevice.isIPad ? .popover : .sheet(detents: [.medium]), isPresented: $showFollowingPrimer) {
FollowingViewModal()
} label: {
EmptyView()
}
}
TabView(selection: $selectedTab) {
if !hideFollowingTab {
NavigationView {

View File

@ -28,7 +28,7 @@ public enum UserDefaultKey: String {
case recentSearchTerms
case audioPlayerExpanded
case themeName
case followingPrimerDisplayed
case stopUsingFollowingPrimer
case notificationsEnabled
case deviceTokenID
case userWordsPerMinute

View File

@ -30,6 +30,10 @@ public extension Image {
static var readerSettings: Image { Image("reader-settings", bundle: .module) }
static var utilityMenu: Image { Image("utility-menu", bundle: .module) }
static var relaxedSlothLight: Image {
Color.isDarkMode ? Image("relaxed-sloth-dark", bundle: .module) : Image("relaxed-sloth-light", bundle: .module)
}
static var addLink: Image { Image("add-link", bundle: .module) }
static var selectMultiple: Image { Image("select-multiple", bundle: .module) }
static var magnifyingGlass: Image { Image("magnifying-glass", bundle: .module) }

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "relaxed-sloth-dark.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "sloth 1.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "sloth 2.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "sloth 1.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "sloth 2.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "sloth 3.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB