From c85c244459926de33aeaff875063a3ec5c544cd4 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 6 Apr 2022 08:28:37 -0700 Subject: [PATCH 01/19] use text chip to display labels in labels view --- apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift index 852b64e8c..7161f67ad 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift @@ -123,7 +123,7 @@ struct LabelsView: View { Section(header: Text("Labels")) { ForEach(viewModel.labels, id: \.id) { label in HStack { - Text(label.name) + TextChip(feedItemLabel: label) Spacer() Button( action: { From e059d2d6e7212aa1ca23dfb5a2e1ac53dce501ec Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 6 Apr 2022 08:53:18 -0700 Subject: [PATCH 02/19] use assigned and unassigned section for label asiignment modal --- .../Sources/App/Views/ApplyLabelsView.swift | 63 +++++++++++++++---- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/ApplyLabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/ApplyLabelsView.swift index ffb6214fb..23e65a0b7 100644 --- a/apple/OmnivoreKit/Sources/App/Views/ApplyLabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/ApplyLabelsView.swift @@ -7,8 +7,8 @@ import Views final class ApplyLabelsViewModel: ObservableObject { private var hasLoadedInitialLabels = false @Published var isLoading = true - @Published var selectedLabels = Set() - @Published var labels = [FeedItemLabel]() + @Published var selectedLabels = [FeedItemLabel]() + @Published var unselectedLabels = [FeedItemLabel]() var subscriptions = Set() @@ -17,11 +17,11 @@ final class ApplyLabelsViewModel: ObservableObject { dataService.labelsPublisher().sink( receiveCompletion: { _ in }, - receiveValue: { [weak self] result in + receiveValue: { [weak self] allLabels in self?.isLoading = false - self?.labels = result self?.hasLoadedInitialLabels = true - self?.selectedLabels = Set(item.labels) + self?.selectedLabels = item.labels + self?.unselectedLabels = allLabels.filter { !item.labels.contains($0) } } ) .store(in: &subscriptions) @@ -34,6 +34,16 @@ final class ApplyLabelsViewModel: ObservableObject { ) .store(in: &subscriptions) } + + func addLabel(_ label: FeedItemLabel) { + selectedLabels.insert(label, at: 0) + unselectedLabels.removeAll { $0.id == label.id } + } + + func removeLabel(_ label: FeedItemLabel) { + unselectedLabels.insert(label, at: 0) + selectedLabels.removeAll { $0.id == label.id } + } } struct ApplyLabelsView: View { @@ -49,15 +59,44 @@ struct ApplyLabelsView: View { if viewModel.isLoading { EmptyView() } else { - List(viewModel.labels, id: \.self, selection: $viewModel.selectedLabels) { label in - if let textChip = TextChip(feedItemLabel: label) { - textChip - } else { - Text(label.name) + List { + Section(header: Text("Assigned Labels")) { + if viewModel.selectedLabels.isEmpty { + Text("No labels are currently assigned.") + } + ForEach(viewModel.selectedLabels, id: \.self) { label in + HStack { + TextChip(feedItemLabel: label) + Spacer() + Button( + action: { + withAnimation { + viewModel.removeLabel(label) + } + }, + label: { Image(systemName: "trash") } + ) + } + } + } + Section(header: Text("Available Labels")) { + ForEach(viewModel.unselectedLabels, id: \.self) { label in + HStack { + TextChip(feedItemLabel: label) + Spacer() + Button( + action: { + withAnimation { + viewModel.addLabel(label) + } + }, + label: { Image(systemName: "plus") } + ) + } + } } } - .environment(\.editMode, .constant(EditMode.active)) - .navigationTitle("Apply Labels") + .navigationTitle("Assign Labels") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { From 5cbfca1fa95b1a7b3d363bfa63912b28ee65dba8 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 6 Apr 2022 09:09:12 -0700 Subject: [PATCH 03/19] add create label option to label assignment modal --- .../Views/{ => Labels}/ApplyLabelsView.swift | 61 +++-------- .../{Profile => Labels}/LabelsView.swift | 59 ---------- .../App/Views/Labels/LabelsViewModel.swift | 103 ++++++++++++++++++ 3 files changed, 120 insertions(+), 103 deletions(-) rename apple/OmnivoreKit/Sources/App/Views/{ => Labels}/ApplyLabelsView.swift (62%) rename apple/OmnivoreKit/Sources/App/Views/{Profile => Labels}/LabelsView.swift (70%) create mode 100644 apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/ApplyLabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift similarity index 62% rename from apple/OmnivoreKit/Sources/App/Views/ApplyLabelsView.swift rename to apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift index 23e65a0b7..f49d8d582 100644 --- a/apple/OmnivoreKit/Sources/App/Views/ApplyLabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift @@ -1,58 +1,15 @@ -import Combine import Models import Services import SwiftUI import Views -final class ApplyLabelsViewModel: ObservableObject { - private var hasLoadedInitialLabels = false - @Published var isLoading = true - @Published var selectedLabels = [FeedItemLabel]() - @Published var unselectedLabels = [FeedItemLabel]() - - var subscriptions = Set() - - func load(item: FeedItem, dataService: DataService) { - guard !hasLoadedInitialLabels else { return } - - dataService.labelsPublisher().sink( - receiveCompletion: { _ in }, - receiveValue: { [weak self] allLabels in - self?.isLoading = false - self?.hasLoadedInitialLabels = true - self?.selectedLabels = item.labels - self?.unselectedLabels = allLabels.filter { !item.labels.contains($0) } - } - ) - .store(in: &subscriptions) - } - - func saveChanges(itemID: String, dataService: DataService, onComplete: @escaping ([FeedItemLabel]) -> Void) { - dataService.updateArticleLabelsPublisher(itemID: itemID, labelIDs: selectedLabels.map(\.id)).sink( - receiveCompletion: { _ in }, - receiveValue: { onComplete($0) } - ) - .store(in: &subscriptions) - } - - func addLabel(_ label: FeedItemLabel) { - selectedLabels.insert(label, at: 0) - unselectedLabels.removeAll { $0.id == label.id } - } - - func removeLabel(_ label: FeedItemLabel) { - unselectedLabels.insert(label, at: 0) - selectedLabels.removeAll { $0.id == label.id } - } -} - struct ApplyLabelsView: View { let item: FeedItem let commitLabelChanges: ([FeedItemLabel]) -> Void @EnvironmentObject var dataService: DataService @Environment(\.presentationMode) private var presentationMode - @StateObject var viewModel = ApplyLabelsViewModel() + @StateObject var viewModel = LabelsViewModel() var body: some View { NavigationView { @@ -95,6 +52,19 @@ struct ApplyLabelsView: View { } } } + Section { + Button( + action: { viewModel.showCreateEmailModal = true }, + label: { + HStack { + Image(systemName: "plus.circle.fill").foregroundColor(.green) + Text("Create a new Label") + Spacer() + } + } + ) + .disabled(viewModel.isLoading) + } } .navigationTitle("Assign Labels") .navigationBarTitleDisplayMode(.inline) @@ -117,6 +87,9 @@ struct ApplyLabelsView: View { ) } } + .sheet(isPresented: $viewModel.showCreateEmailModal) { + CreateLabelView(viewModel: viewModel) + } } } .onAppear { diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift similarity index 70% rename from apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift rename to apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift index 7161f67ad..5d535b0d8 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/LabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift @@ -4,65 +4,6 @@ import Services import SwiftUI import Views -final class LabelsViewModel: ObservableObject { - private var hasLoadedInitialLabels = false - @Published var isLoading = false - @Published var labels = [FeedItemLabel]() - @Published var showCreateEmailModal = false - - var subscriptions = Set() - - func loadLabels(dataService: DataService) { - guard !hasLoadedInitialLabels else { return } - isLoading = true - - dataService.labelsPublisher().sink( - receiveCompletion: { _ in }, - receiveValue: { [weak self] result in - self?.isLoading = false - self?.labels = result - self?.hasLoadedInitialLabels = true - } - ) - .store(in: &subscriptions) - } - - func createLabel(dataService: DataService, name: String, color: Color, description: String?) { - isLoading = true - - dataService.createLabelPublisher( - name: name, - color: color.hex ?? "", - description: description - ).sink( - receiveCompletion: { [weak self] _ in - self?.isLoading = false - }, - receiveValue: { [weak self] result in - self?.isLoading = false - self?.labels.insert(result, at: 0) - self?.showCreateEmailModal = false - } - ) - .store(in: &subscriptions) - } - - func deleteLabel(dataService: DataService, labelID: String) { - isLoading = true - - dataService.removeLabelPublisher(labelID: labelID).sink( - receiveCompletion: { [weak self] _ in - self?.isLoading = false - }, - receiveValue: { [weak self] _ in - self?.isLoading = false - self?.labels.removeAll { $0.id == labelID } - } - ) - .store(in: &subscriptions) - } -} - struct LabelsView: View { @EnvironmentObject var dataService: DataService @StateObject var viewModel = LabelsViewModel() diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift new file mode 100644 index 000000000..4d06f287c --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift @@ -0,0 +1,103 @@ +import Combine +import Models +import Services +import SwiftUI +import Views + +final class LabelsViewModel: ObservableObject { + private var hasLoadedInitialLabels = false + @Published var isLoading = false + @Published var selectedLabels = [FeedItemLabel]() + @Published var unselectedLabels = [FeedItemLabel]() + @Published var labels = [FeedItemLabel]() + @Published var showCreateEmailModal = false + + var subscriptions = Set() + + func loadLabels(dataService: DataService) { + guard !hasLoadedInitialLabels else { return } + isLoading = true + + dataService.labelsPublisher().sink( + receiveCompletion: { _ in }, + receiveValue: { [weak self] result in + self?.isLoading = false + self?.labels = result + self?.hasLoadedInitialLabels = true + } + ) + .store(in: &subscriptions) + } + + func load(item: FeedItem, dataService: DataService) { + guard !hasLoadedInitialLabels else { return } + + dataService.labelsPublisher().sink( + receiveCompletion: { _ in }, + receiveValue: { [weak self] allLabels in + self?.isLoading = false + self?.hasLoadedInitialLabels = true + self?.selectedLabels = item.labels + self?.unselectedLabels = allLabels.filter { !item.labels.contains($0) } + } + ) + .store(in: &subscriptions) + } + + func createLabel(dataService: DataService, name: String, color: Color, description: String?) { + isLoading = true + + dataService.createLabelPublisher( + name: name, + color: color.hex ?? "", + description: description + ).sink( + receiveCompletion: { [weak self] _ in + self?.isLoading = false + }, + receiveValue: { [weak self] result in + self?.isLoading = false + self?.labels.insert(result, at: 0) + self?.unselectedLabels.insert(result, at: 0) + self?.showCreateEmailModal = false + } + ) + .store(in: &subscriptions) + } + + func deleteLabel(dataService: DataService, labelID: String) { + isLoading = true + + dataService.removeLabelPublisher(labelID: labelID).sink( + receiveCompletion: { [weak self] _ in + self?.isLoading = false + }, + receiveValue: { [weak self] _ in + self?.isLoading = false + self?.labels.removeAll { $0.id == labelID } + } + ) + .store(in: &subscriptions) + } + + func saveChanges(itemID: String, dataService: DataService, onComplete: @escaping ([FeedItemLabel]) -> Void) { + isLoading = true + dataService.updateArticleLabelsPublisher(itemID: itemID, labelIDs: selectedLabels.map(\.id)).sink( + receiveCompletion: { [weak self] _ in + self?.isLoading = false + }, + receiveValue: { onComplete($0) } + ) + .store(in: &subscriptions) + } + + func addLabel(_ label: FeedItemLabel) { + selectedLabels.insert(label, at: 0) + unselectedLabels.removeAll { $0.id == label.id } + } + + func removeLabel(_ label: FeedItemLabel) { + unselectedLabels.insert(label, at: 0) + selectedLabels.removeAll { $0.id == label.id } + } +} From e9589fd7f38b11f171e134854f8fc86a8f8f72a2 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 6 Apr 2022 09:19:15 -0700 Subject: [PATCH 04/19] apply correct tint colors to label modal buttons --- .../App/Views/Labels/ApplyLabelsView.swift | 24 ++++++++-------- .../Sources/App/Views/Labels/LabelsView.swift | 2 +- .../App/Views/Labels/LabelsViewModel.swift | 28 +++++++++---------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift index f49d8d582..9b2dd7335 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift @@ -18,36 +18,36 @@ struct ApplyLabelsView: View { } else { List { Section(header: Text("Assigned Labels")) { - if viewModel.selectedLabels.isEmpty { + if viewModel.selectedLabelsForItemInContext.isEmpty { Text("No labels are currently assigned.") } - ForEach(viewModel.selectedLabels, id: \.self) { label in + ForEach(viewModel.selectedLabelsForItemInContext, id: \.self) { label in HStack { TextChip(feedItemLabel: label) Spacer() Button( action: { withAnimation { - viewModel.removeLabel(label) + viewModel.removeLabelFromItem(label) } }, - label: { Image(systemName: "trash") } + label: { Image(systemName: "trash").foregroundColor(.appGrayTextContrast) } ) } } } Section(header: Text("Available Labels")) { - ForEach(viewModel.unselectedLabels, id: \.self) { label in + ForEach(viewModel.unselectedLabelsForItemInContext, id: \.self) { label in HStack { TextChip(feedItemLabel: label) Spacer() Button( action: { withAnimation { - viewModel.addLabel(label) + viewModel.addLabelToItem(label) } }, - label: { Image(systemName: "plus") } + label: { Image(systemName: "plus").foregroundColor(.appGrayTextContrast) } ) } } @@ -58,7 +58,7 @@ struct ApplyLabelsView: View { label: { HStack { Image(systemName: "plus.circle.fill").foregroundColor(.green) - Text("Create a new Label") + Text("Create a new Label").foregroundColor(.appGrayTextContrast) Spacer() } } @@ -72,18 +72,18 @@ struct ApplyLabelsView: View { ToolbarItem(placement: .navigationBarLeading) { Button( action: { presentationMode.wrappedValue.dismiss() }, - label: { Text("Cancel") } + label: { Text("Cancel").foregroundColor(.appGrayTextContrast) } ) } ToolbarItem(placement: .navigationBarTrailing) { Button( action: { - viewModel.saveChanges(itemID: item.id, dataService: dataService) { labels in + viewModel.saveItemLabelChanges(itemID: item.id, dataService: dataService) { labels in commitLabelChanges(labels) presentationMode.wrappedValue.dismiss() } }, - label: { Text("Save") } + label: { Text("Save").foregroundColor(.appGrayTextContrast) } ) } } @@ -93,7 +93,7 @@ struct ApplyLabelsView: View { } } .onAppear { - viewModel.load(item: item, dataService: dataService) + viewModel.loadLabelForItem(item: item, dataService: dataService) } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift index 5d535b0d8..d727f5a32 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift @@ -52,7 +52,7 @@ struct LabelsView: View { label: { HStack { Image(systemName: "plus.circle.fill").foregroundColor(.green) - Text("Create a new Label") + Text("Create a new Label").foregroundColor(.appGrayTextContrast) Spacer() } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift index 4d06f287c..8825a8cd7 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift @@ -7,8 +7,8 @@ import Views final class LabelsViewModel: ObservableObject { private var hasLoadedInitialLabels = false @Published var isLoading = false - @Published var selectedLabels = [FeedItemLabel]() - @Published var unselectedLabels = [FeedItemLabel]() + @Published var selectedLabelsForItemInContext = [FeedItemLabel]() + @Published var unselectedLabelsForItemInContext = [FeedItemLabel]() @Published var labels = [FeedItemLabel]() @Published var showCreateEmailModal = false @@ -29,7 +29,7 @@ final class LabelsViewModel: ObservableObject { .store(in: &subscriptions) } - func load(item: FeedItem, dataService: DataService) { + func loadLabelForItem(item: FeedItem, dataService: DataService) { guard !hasLoadedInitialLabels else { return } dataService.labelsPublisher().sink( @@ -37,8 +37,8 @@ final class LabelsViewModel: ObservableObject { receiveValue: { [weak self] allLabels in self?.isLoading = false self?.hasLoadedInitialLabels = true - self?.selectedLabels = item.labels - self?.unselectedLabels = allLabels.filter { !item.labels.contains($0) } + self?.selectedLabelsForItemInContext = item.labels + self?.unselectedLabelsForItemInContext = allLabels.filter { !item.labels.contains($0) } } ) .store(in: &subscriptions) @@ -58,7 +58,7 @@ final class LabelsViewModel: ObservableObject { receiveValue: { [weak self] result in self?.isLoading = false self?.labels.insert(result, at: 0) - self?.unselectedLabels.insert(result, at: 0) + self?.unselectedLabelsForItemInContext.insert(result, at: 0) self?.showCreateEmailModal = false } ) @@ -80,9 +80,9 @@ final class LabelsViewModel: ObservableObject { .store(in: &subscriptions) } - func saveChanges(itemID: String, dataService: DataService, onComplete: @escaping ([FeedItemLabel]) -> Void) { + func saveItemLabelChanges(itemID: String, dataService: DataService, onComplete: @escaping ([FeedItemLabel]) -> Void) { isLoading = true - dataService.updateArticleLabelsPublisher(itemID: itemID, labelIDs: selectedLabels.map(\.id)).sink( + dataService.updateArticleLabelsPublisher(itemID: itemID, labelIDs: selectedLabelsForItemInContext.map(\.id)).sink( receiveCompletion: { [weak self] _ in self?.isLoading = false }, @@ -91,13 +91,13 @@ final class LabelsViewModel: ObservableObject { .store(in: &subscriptions) } - func addLabel(_ label: FeedItemLabel) { - selectedLabels.insert(label, at: 0) - unselectedLabels.removeAll { $0.id == label.id } + func addLabelToItem(_ label: FeedItemLabel) { + selectedLabelsForItemInContext.insert(label, at: 0) + unselectedLabelsForItemInContext.removeAll { $0.id == label.id } } - func removeLabel(_ label: FeedItemLabel) { - unselectedLabels.insert(label, at: 0) - selectedLabels.removeAll { $0.id == label.id } + func removeLabelFromItem(_ label: FeedItemLabel) { + unselectedLabelsForItemInContext.insert(label, at: 0) + selectedLabelsForItemInContext.removeAll { $0.id == label.id } } } From 8329cc166cb8db171160d81133a7690e3d9096f1 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 6 Apr 2022 09:36:37 -0700 Subject: [PATCH 05/19] fix filter application --- .../App/Views/Labels/ApplyLabelsView.swift | 2 +- .../Sources/App/Views/Labels/LabelsView.swift | 2 +- .../App/Views/Labels/LabelsViewModel.swift | 25 ++++++------------- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift index 9b2dd7335..d2227a3ed 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift @@ -93,7 +93,7 @@ struct ApplyLabelsView: View { } } .onAppear { - viewModel.loadLabelForItem(item: item, dataService: dataService) + viewModel.loadLabels(dataService: dataService, item: item) } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift index d727f5a32..e89c6144f 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift @@ -41,7 +41,7 @@ struct LabelsView: View { .listStyle(InsetListStyle()) #endif } - .onAppear { viewModel.loadLabels(dataService: dataService) } + .onAppear { viewModel.loadLabels(dataService: dataService, item: nil) } } private var innerBody: some View { diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift index 8825a8cd7..b38109c72 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift @@ -14,31 +14,22 @@ final class LabelsViewModel: ObservableObject { var subscriptions = Set() - func loadLabels(dataService: DataService) { + func loadLabels(dataService: DataService, item: FeedItem?) { guard !hasLoadedInitialLabels else { return } isLoading = true - dataService.labelsPublisher().sink( - receiveCompletion: { _ in }, - receiveValue: { [weak self] result in - self?.isLoading = false - self?.labels = result - self?.hasLoadedInitialLabels = true - } - ) - .store(in: &subscriptions) - } - - func loadLabelForItem(item: FeedItem, dataService: DataService) { - guard !hasLoadedInitialLabels else { return } - dataService.labelsPublisher().sink( receiveCompletion: { _ in }, receiveValue: { [weak self] allLabels in self?.isLoading = false + self?.labels = allLabels self?.hasLoadedInitialLabels = true - self?.selectedLabelsForItemInContext = item.labels - self?.unselectedLabelsForItemInContext = allLabels.filter { !item.labels.contains($0) } + if let item = item { + self?.selectedLabelsForItemInContext = item.labels + self?.unselectedLabelsForItemInContext = allLabels.filter { label in + !item.labels.contains(where: { $0.id == label.id }) + } + } } ) .store(in: &subscriptions) From 29d10feb61c64394b2854f9a07c23d607a5061af Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 6 Apr 2022 20:47:25 -0700 Subject: [PATCH 06/19] move edit label sheet so refreshable modifier doesn't get applied to it --- .../Sources/App/Views/Home/HomeFeedViewIOS.swift | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 535eb577e..a45f7daad 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -49,6 +49,11 @@ import Views .onSubmit(of: .search) { viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) } + .sheet(item: $viewModel.itemUnderLabelEdit) { item in + ApplyLabelsView(item: item) { labels in + viewModel.updateLabels(itemID: item.id, labels: labels) + } + } } else { HomeFeedView( prefersListLayout: $prefersListLayout, @@ -58,6 +63,11 @@ import Views itemToSnooze: $itemToSnooze, viewModel: viewModel ) + .sheet(item: $viewModel.itemUnderLabelEdit) { item in + ApplyLabelsView(item: item) { labels in + viewModel.updateLabels(itemID: item.id, labels: labels) + } + } .toolbar { ToolbarItem { if viewModel.isLoading { @@ -167,11 +177,6 @@ import Views } } } - .sheet(item: $viewModel.itemUnderLabelEdit) { item in - ApplyLabelsView(item: item) { labels in - viewModel.updateLabels(itemID: item.id, labels: labels) - } - } } } } From 287da3d93954d81d447612782aea3c7c3df45d6e Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 6 Apr 2022 21:30:38 -0700 Subject: [PATCH 07/19] use searchable to filter labels --- .../App/Views/Labels/ApplyLabelsView.swift | 167 ++++++++++-------- 1 file changed, 95 insertions(+), 72 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift index d2227a3ed..95ff997dd 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift @@ -10,85 +10,99 @@ struct ApplyLabelsView: View { @EnvironmentObject var dataService: DataService @Environment(\.presentationMode) private var presentationMode @StateObject var viewModel = LabelsViewModel() + @State private var labelSearchFilter = "" + + var innerBody: some View { + List { + Section(header: Text("Assigned Labels")) { + if viewModel.selectedLabelsForItemInContext.isEmpty { + Text("No labels are currently assigned.") + } + ForEach(viewModel.selectedLabelsForItemInContext.applySearchFilter(labelSearchFilter), id: \.self) { label in + HStack { + TextChip(feedItemLabel: label) + Spacer() + Button( + action: { + withAnimation { + viewModel.removeLabelFromItem(label) + } + }, + label: { Image(systemName: "trash").foregroundColor(.appGrayTextContrast) } + ) + } + } + } + Section(header: Text("Available Labels")) { + ForEach(viewModel.unselectedLabelsForItemInContext.applySearchFilter(labelSearchFilter), id: \.self) { label in + HStack { + TextChip(feedItemLabel: label) + Spacer() + Button( + action: { + withAnimation { + viewModel.addLabelToItem(label) + } + }, + label: { Image(systemName: "plus").foregroundColor(.appGrayTextContrast) } + ) + } + } + } + Section { + Button( + action: { viewModel.showCreateEmailModal = true }, + label: { + HStack { + Image(systemName: "plus.circle.fill").foregroundColor(.green) + Text("Create a new Label").foregroundColor(.appGrayTextContrast) + Spacer() + } + } + ) + .disabled(viewModel.isLoading) + } + } + .navigationTitle("Assign Labels") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button( + action: { presentationMode.wrappedValue.dismiss() }, + label: { Text("Cancel").foregroundColor(.appGrayTextContrast) } + ) + } + ToolbarItem(placement: .navigationBarTrailing) { + Button( + action: { + viewModel.saveItemLabelChanges(itemID: item.id, dataService: dataService) { labels in + commitLabelChanges(labels) + presentationMode.wrappedValue.dismiss() + } + }, + label: { Text("Save").foregroundColor(.appGrayTextContrast) } + ) + } + } + .sheet(isPresented: $viewModel.showCreateEmailModal) { + CreateLabelView(viewModel: viewModel) + } + } var body: some View { NavigationView { if viewModel.isLoading { EmptyView() + } else { - List { - Section(header: Text("Assigned Labels")) { - if viewModel.selectedLabelsForItemInContext.isEmpty { - Text("No labels are currently assigned.") - } - ForEach(viewModel.selectedLabelsForItemInContext, id: \.self) { label in - HStack { - TextChip(feedItemLabel: label) - Spacer() - Button( - action: { - withAnimation { - viewModel.removeLabelFromItem(label) - } - }, - label: { Image(systemName: "trash").foregroundColor(.appGrayTextContrast) } - ) - } - } - } - Section(header: Text("Available Labels")) { - ForEach(viewModel.unselectedLabelsForItemInContext, id: \.self) { label in - HStack { - TextChip(feedItemLabel: label) - Spacer() - Button( - action: { - withAnimation { - viewModel.addLabelToItem(label) - } - }, - label: { Image(systemName: "plus").foregroundColor(.appGrayTextContrast) } - ) - } - } - } - Section { - Button( - action: { viewModel.showCreateEmailModal = true }, - label: { - HStack { - Image(systemName: "plus.circle.fill").foregroundColor(.green) - Text("Create a new Label").foregroundColor(.appGrayTextContrast) - Spacer() - } - } + if #available(iOS 15.0, *) { + innerBody + .searchable( + text: $labelSearchFilter, + placement: .navigationBarDrawer(displayMode: .always) ) - .disabled(viewModel.isLoading) - } - } - .navigationTitle("Assign Labels") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button( - action: { presentationMode.wrappedValue.dismiss() }, - label: { Text("Cancel").foregroundColor(.appGrayTextContrast) } - ) - } - ToolbarItem(placement: .navigationBarTrailing) { - Button( - action: { - viewModel.saveItemLabelChanges(itemID: item.id, dataService: dataService) { labels in - commitLabelChanges(labels) - presentationMode.wrappedValue.dismiss() - } - }, - label: { Text("Save").foregroundColor(.appGrayTextContrast) } - ) - } - } - .sheet(isPresented: $viewModel.showCreateEmailModal) { - CreateLabelView(viewModel: viewModel) + } else { + innerBody } } } @@ -97,3 +111,12 @@ struct ApplyLabelsView: View { } } } + +private extension Sequence where Element == FeedItemLabel { + func applySearchFilter(_ searchFilter: String) -> [FeedItemLabel] { + if searchFilter.isEmpty { + return map { $0 } // return the identity of the sequence + } + return filter { $0.name.lowercased().contains(searchFilter.lowercased()) } + } +} From 9fb0e1112480150f65ea72f7d84b6aceb912a594 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 7 Apr 2022 13:30:18 -0700 Subject: [PATCH 08/19] udpate text chip colors --- apple/OmnivoreKit/Sources/Utils/ColorUtils.swift | 12 ++++++++++-- apple/OmnivoreKit/Sources/Views/TextChip.swift | 8 ++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift b/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift index 7718fbf88..2c20306f6 100644 --- a/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift +++ b/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift @@ -45,8 +45,7 @@ public extension Color { } private func toHex() -> String? { - let uic = UIColor(self) - guard let components = uic.cgColor.components, components.count >= 3 else { + guard let components = UIColor(self).cgColor.components, components.count >= 3 else { return nil } let red = Float(components[0]) @@ -60,4 +59,13 @@ public extension Color { lroundf(blue * 255) ) } + + var isDark: Bool { + guard let components = UIColor(self).cgColor.components, components.count >= 3 else { + return false + } + + let lum = 0.2126 * Float(components[0]) + 0.7152 * Float(components[1]) + 0.0722 * Float(components[2]) + return lum < 0.50 + } } diff --git a/apple/OmnivoreKit/Sources/Views/TextChip.swift b/apple/OmnivoreKit/Sources/Views/TextChip.swift index b25bb7c94..cb55573e8 100644 --- a/apple/OmnivoreKit/Sources/Views/TextChip.swift +++ b/apple/OmnivoreKit/Sources/Views/TextChip.swift @@ -24,13 +24,9 @@ public struct TextChip: View { .padding(.horizontal, 10) .padding(.vertical, 5) .font(.appFootnote) - .foregroundColor(color) + .foregroundColor(color.isDark ? .white : .black) .lineLimit(1) - .background(color.opacity(0.1)) + .background(color) .cornerRadius(cornerRadius) - .overlay( - RoundedRectangle(cornerRadius: cornerRadius) - .stroke(color.opacity(0.3), lineWidth: 1) - ) } } From 90a0d7c1c74097d1fc86c766cf7158fb72eb41ac Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 8 Apr 2022 08:30:55 -0700 Subject: [PATCH 09/19] move searchQuery state to homeViewModel --- .../Components/FeedCardNavigationLink.swift | 6 ++-- .../App/Views/Home/HomeFeedViewIOS.swift | 32 +++++++------------ .../App/Views/Home/HomeFeedViewModel.swift | 10 +++--- 3 files changed, 19 insertions(+), 29 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift index 9db48b14a..1c19b5398 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift @@ -7,7 +7,6 @@ struct FeedCardNavigationLink: View { @EnvironmentObject var dataService: DataService let item: FeedItem - let searchQuery: String @Binding var selectedLinkItem: FeedItem? @@ -25,7 +24,7 @@ struct FeedCardNavigationLink: View { .opacity(0) .buttonStyle(PlainButtonStyle()) .onAppear { - viewModel.itemAppeared(item: item, searchQuery: searchQuery, dataService: dataService) + viewModel.itemAppeared(item: item, dataService: dataService) } FeedCard(item: item) } @@ -38,7 +37,6 @@ struct GridCardNavigationLink: View { @State private var scale = 1.0 let item: FeedItem - let searchQuery: String let actionHandler: (GridCardAction) -> Void @Binding var selectedLinkItem: FeedItem? @@ -65,7 +63,7 @@ struct GridCardNavigationLink: View { } }) .onAppear { - viewModel.itemAppeared(item: item, searchQuery: searchQuery, dataService: dataService) + viewModel.itemAppeared(item: item, dataService: dataService) } } .aspectRatio(1.8, contentMode: .fill) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index a45f7daad..1991ede19 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -10,7 +10,6 @@ import Views struct HomeFeedContainerView: View { @EnvironmentObject var dataService: DataService @AppStorage(UserDefaultKey.homeFeedlayoutPreference.rawValue) var prefersListLayout = UIDevice.isIPhone - @State private var searchQuery = "" @State private var snoozePresented = false @State private var itemToSnooze: FeedItem? @State private var selectedLinkItem: FeedItem? @@ -21,33 +20,32 @@ import Views if #available(iOS 15.0, *) { HomeFeedView( prefersListLayout: $prefersListLayout, - searchQuery: $searchQuery, selectedLinkItem: $selectedLinkItem, snoozePresented: $snoozePresented, itemToSnooze: $itemToSnooze, viewModel: viewModel ) .refreshable { - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) } .searchable( - text: $searchQuery, + text: $viewModel.searchQuery, placement: .sidebar ) { - if searchQuery.isEmpty { + if viewModel.searchQuery.isEmpty { Text("Inbox").searchCompletion("in:inbox ") Text("All").searchCompletion("in:all ") Text("Archived").searchCompletion("in:archive ") Text("Files").searchCompletion("type:file ") } } - .onChange(of: searchQuery) { _ in + .onChange(of: viewModel.searchQuery) { _ in // Maybe we should debounce this, but // it feels like it works ok without - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) } .onSubmit(of: .search) { - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) } .sheet(item: $viewModel.itemUnderLabelEdit) { item in ApplyLabelsView(item: item) { labels in @@ -57,7 +55,6 @@ import Views } else { HomeFeedView( prefersListLayout: $prefersListLayout, - searchQuery: $searchQuery, selectedLinkItem: $selectedLinkItem, snoozePresented: $snoozePresented, itemToSnooze: $itemToSnooze, @@ -74,7 +71,7 @@ import Views Button(action: {}, label: { ProgressView() }) } else { Button( - action: { viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) }, + action: { viewModel.loadItems(dataService: dataService, isRefresh: true) }, label: { Label("Refresh Feed", systemImage: "arrow.clockwise") } ) } @@ -86,7 +83,7 @@ import Views .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in // Don't refresh the list if the user is currently reading an article if selectedLinkItem == nil { - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) } } .onReceive(NotificationCenter.default.publisher(for: Notification.Name("PushFeedItem"))) { notification in @@ -107,7 +104,7 @@ import Views } .onAppear { if viewModel.items.isEmpty { - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) } } .onChange(of: selectedLinkItem) { _ in @@ -120,7 +117,6 @@ import Views @EnvironmentObject var dataService: DataService @Binding var prefersListLayout: Bool - @Binding var searchQuery: String @Binding var selectedLinkItem: FeedItem? @Binding var snoozePresented: Bool @Binding var itemToSnooze: FeedItem? @@ -131,7 +127,6 @@ import Views if prefersListLayout { HomeFeedListView( prefersListLayout: $prefersListLayout, - searchQuery: $searchQuery, selectedLinkItem: $selectedLinkItem, snoozePresented: $snoozePresented, itemToSnooze: $itemToSnooze, @@ -139,7 +134,6 @@ import Views ) } else { HomeFeedGridView( - searchQuery: $searchQuery, selectedLinkItem: $selectedLinkItem, snoozePresented: $snoozePresented, itemToSnooze: $itemToSnooze, @@ -160,7 +154,7 @@ import Views Button(action: {}, label: { ProgressView() }) } else { Button( - action: { viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) }, + action: { viewModel.loadItems(dataService: dataService, isRefresh: true) }, label: { Label("Refresh Feed", systemImage: "arrow.clockwise") } ) } @@ -184,7 +178,6 @@ import Views struct HomeFeedListView: View { @EnvironmentObject var dataService: DataService @Binding var prefersListLayout: Bool - @Binding var searchQuery: String @Binding var selectedLinkItem: FeedItem? @Binding var snoozePresented: Bool @Binding var itemToSnooze: FeedItem? @@ -200,7 +193,6 @@ import Views ForEach(viewModel.items) { item in let link = FeedCardNavigationLink( item: item, - searchQuery: searchQuery, selectedLinkItem: $selectedLinkItem, viewModel: viewModel ) @@ -315,7 +307,6 @@ import Views struct HomeFeedGridView: View { @EnvironmentObject var dataService: DataService - @Binding var searchQuery: String @Binding var selectedLinkItem: FeedItem? @Binding var snoozePresented: Bool @Binding var itemToSnooze: FeedItem? @@ -344,7 +335,6 @@ import Views ForEach(viewModel.items) { item in let link = GridCardNavigationLink( item: item, - searchQuery: searchQuery, actionHandler: { contextMenuActionHandler(item: item, action: $0) }, selectedLinkItem: $selectedLinkItem, isContextMenuOpen: $isContextMenuOpen, @@ -380,7 +370,7 @@ import Views .onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { offset in DispatchQueue.main.async { if !viewModel.isLoading, offset > 240 { - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index f5e304094..18ef3b6e7 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -15,6 +15,8 @@ final class HomeFeedViewModel: ObservableObject { @Published var isLoading = false @Published var showPushNotificationPrimer = false @Published var itemUnderLabelEdit: FeedItem? + @Published var searchQuery = "" + var cursor: String? var sendProgressUpdates = false @@ -27,14 +29,14 @@ final class HomeFeedViewModel: ObservableObject { init() {} - func itemAppeared(item: FeedItem, searchQuery: String, dataService: DataService) { + func itemAppeared(item: FeedItem, dataService: DataService) { if isLoading { return } let itemIndex = items.firstIndex(where: { $0.id == item.id }) let thresholdIndex = items.index(items.endIndex, offsetBy: -5) // Check if user has scrolled to the last five items in the list if let itemIndex = itemIndex, itemIndex > thresholdIndex, items.count < thresholdIndex + 10 { - loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: false) + loadItems(dataService: dataService, isRefresh: false) } } @@ -42,7 +44,7 @@ final class HomeFeedViewModel: ObservableObject { items.insert(item, at: 0) } - func loadItems(dataService: DataService, searchQuery: String?, isRefresh: Bool) { + func loadItems(dataService: DataService, isRefresh: Bool) { // Clear offline highlights since we'll be populating new FeedItems with the correct highlights set dataService.clearHighlights() @@ -63,7 +65,7 @@ final class HomeFeedViewModel: ObservableObject { dataService.libraryItemsPublisher( limit: 10, sortDescending: true, - searchQuery: searchQuery, + searchQuery: searchQuery.isEmpty ? nil : searchQuery, cursor: isRefresh ? nil : cursor ) .sink( From 1bdd79e873006b430937e2a2c37d2d710f8f06c9 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 8 Apr 2022 08:45:30 -0700 Subject: [PATCH 10/19] update macos app --- .../App/Views/Home/HomeFeedViewMac.swift | 14 +- .../App/Views/Labels/ApplyLabelsView.swift | 61 ++- .../Sources/App/Views/Labels/LabelsView.swift | 8 +- .../App/Views/WebReader/WebReader.swift | 172 +++--- .../Views/WebReader/WebReaderContainer.swift | 496 +++++++++--------- .../WebReader/WebReaderCoordinator.swift | 4 +- .../Sources/Utils/ColorUtils.swift | 28 +- .../Sources/Views/Article/WebAppView.swift | 4 +- .../Sources/Views/Article/WebView.swift | 2 +- 9 files changed, 410 insertions(+), 379 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift index bd4849c14..caed1c399 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift @@ -33,7 +33,6 @@ import Views ForEach(viewModel.items) { item in FeedCardNavigationLink( item: item, - searchQuery: searchQuery, selectedLinkItem: $selectedLinkItem, viewModel: viewModel ) @@ -96,16 +95,16 @@ import Views .onChange(of: searchQuery) { _ in // Maybe we should debounce this, but // it feels like it works ok without - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) } .onSubmit(of: .search) { - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) } .toolbar { ToolbarItem { Button( action: { - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) }, label: { Label("Refresh Feed", systemImage: "arrow.clockwise") } ) @@ -120,7 +119,7 @@ import Views } .onAppear { if viewModel.items.isEmpty { - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) } } } @@ -131,7 +130,6 @@ import Views ForEach(viewModel.items) { item in FeedCardNavigationLink( item: item, - searchQuery: searchQuery, selectedLinkItem: $selectedLinkItem, viewModel: viewModel ) @@ -148,7 +146,7 @@ import Views ToolbarItem { Button( action: { - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) }, label: { Label("Refresh Feed", systemImage: "arrow.clockwise") } ) @@ -156,7 +154,7 @@ import Views } .onAppear { if viewModel.items.isEmpty { - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift index 95ff997dd..1c1348c94 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift @@ -64,26 +64,28 @@ struct ApplyLabelsView: View { } } .navigationTitle("Assign Labels") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button( - action: { presentationMode.wrappedValue.dismiss() }, - label: { Text("Cancel").foregroundColor(.appGrayTextContrast) } - ) + #if os(iOS) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button( + action: { presentationMode.wrappedValue.dismiss() }, + label: { Text("Cancel").foregroundColor(.appGrayTextContrast) } + ) + } + ToolbarItem(placement: .navigationBarTrailing) { + Button( + action: { + viewModel.saveItemLabelChanges(itemID: item.id, dataService: dataService) { labels in + commitLabelChanges(labels) + presentationMode.wrappedValue.dismiss() + } + }, + label: { Text("Save").foregroundColor(.appGrayTextContrast) } + ) + } } - ToolbarItem(placement: .navigationBarTrailing) { - Button( - action: { - viewModel.saveItemLabelChanges(itemID: item.id, dataService: dataService) { labels in - commitLabelChanges(labels) - presentationMode.wrappedValue.dismiss() - } - }, - label: { Text("Save").foregroundColor(.appGrayTextContrast) } - ) - } - } + #endif .sheet(isPresented: $viewModel.showCreateEmailModal) { CreateLabelView(viewModel: viewModel) } @@ -93,17 +95,20 @@ struct ApplyLabelsView: View { NavigationView { if viewModel.isLoading { EmptyView() - } else { - if #available(iOS 15.0, *) { + #if os(iOS) + if #available(iOS 15.0, *) { + innerBody + .searchable( + text: $labelSearchFilter, + placement: .navigationBarDrawer(displayMode: .always) + ) + } else { + innerBody + } + #else innerBody - .searchable( - text: $labelSearchFilter, - placement: .navigationBarDrawer(displayMode: .always) - ) - } else { - innerBody - } + #endif } } .onAppear { diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift index e89c6144f..627305438 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift @@ -96,8 +96,10 @@ struct CreateLabelView: View { NavigationView { VStack(spacing: 16) { TextField("Label Name", text: $newLabelName) + #if os(iOS) .keyboardType(.alphabet) - .textFieldStyle(StandardTextFieldStyle()) + #endif + .textFieldStyle(StandardTextFieldStyle()) ColorPicker( newLabelColor == .clear ? "Select Color" : newLabelColor.description, selection: $newLabelColor @@ -130,7 +132,9 @@ struct CreateLabelView: View { } } .navigationTitle("Create New Label") - .navigationBarTitleDisplayMode(.inline) + #if os(iOS) + .navigationBarTitleDisplayMode(.inline) + #endif } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift index 26ee03a8d..a4544fafb 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift @@ -4,107 +4,109 @@ import Utils import Views import WebKit -struct WebReader: UIViewRepresentable { - let articleContent: ArticleContent - let item: FeedItem - let openLinkAction: (URL) -> Void - let webViewActionHandler: (WKScriptMessage, WKScriptMessageReplyHandler?) -> Void - let navBarVisibilityRatioUpdater: (Double) -> Void +#if os(iOS) + struct WebReader: UIViewRepresentable { + let articleContent: ArticleContent + let item: FeedItem + let openLinkAction: (URL) -> Void + let webViewActionHandler: (WKScriptMessage, WKScriptMessageReplyHandler?) -> Void + let navBarVisibilityRatioUpdater: (Double) -> Void - @Binding var increaseFontActionID: UUID? - @Binding var decreaseFontActionID: UUID? - @Binding var annotationSaveTransactionID: UUID? - @Binding var annotation: String + @Binding var increaseFontActionID: UUID? + @Binding var decreaseFontActionID: UUID? + @Binding var annotationSaveTransactionID: UUID? + @Binding var annotation: String - func makeCoordinator() -> WebReaderCoordinator { - WebReaderCoordinator() - } - - func fontSize() -> Int { - let storedSize = UserDefaults.standard.integer(forKey: UserDefaultKey.preferredWebFontSize.rawValue) - return storedSize <= 1 ? UITraitCollection.current.preferredWebFontSize : storedSize - } - - func makeUIView(context: Context) -> WKWebView { - let webView = WebViewManager.shared() - let contentController = WKUserContentController() - - webView.navigationDelegate = context.coordinator - webView.isOpaque = false - webView.backgroundColor = .clear - webView.configuration.userContentController = contentController - webView.scrollView.delegate = context.coordinator - webView.scrollView.contentInset.top = readerViewNavBarHeight - webView.scrollView.verticalScrollIndicatorInsets.top = readerViewNavBarHeight - - webView.configuration.userContentController.removeAllScriptMessageHandlers() - - for action in WebViewAction.allCases { - webView.configuration.userContentController.add(context.coordinator, name: action.rawValue) + func makeCoordinator() -> WebReaderCoordinator { + WebReaderCoordinator() } - webView.configuration.userContentController.add(webView, name: "viewerAction") - - webView.configuration.userContentController.addScriptMessageHandler( - context.coordinator, contentWorld: .page, name: "articleAction" - ) - - context.coordinator.linkHandler = openLinkAction - context.coordinator.webViewActionHandler = webViewActionHandler - context.coordinator.updateNavBarVisibilityRatio = navBarVisibilityRatioUpdater - loadContent(webView: webView) - - return webView - } - - func updateUIView(_ webView: WKWebView, context: Context) { - if annotationSaveTransactionID != context.coordinator.lastSavedAnnotationID { - context.coordinator.lastSavedAnnotationID = annotationSaveTransactionID - (webView as? WebView)?.saveAnnotation(annotation: annotation) + func fontSize() -> Int { + let storedSize = UserDefaults.standard.integer(forKey: UserDefaultKey.preferredWebFontSize.rawValue) + return storedSize <= 1 ? UITraitCollection.current.preferredWebFontSize : storedSize } - if increaseFontActionID != context.coordinator.previousIncreaseFontActionID { - context.coordinator.previousIncreaseFontActionID = increaseFontActionID - (webView as? WebView)?.increaseFontSize() - } + func makeUIView(context: Context) -> WKWebView { + let webView = WebViewManager.shared() + let contentController = WKUserContentController() - if decreaseFontActionID != context.coordinator.previousDecreaseFontActionID { - context.coordinator.previousDecreaseFontActionID = decreaseFontActionID - (webView as? WebView)?.decreaseFontSize() - } + webView.navigationDelegate = context.coordinator + webView.isOpaque = false + webView.backgroundColor = .clear + webView.configuration.userContentController = contentController + webView.scrollView.delegate = context.coordinator + webView.scrollView.contentInset.top = readerViewNavBarHeight + webView.scrollView.verticalScrollIndicatorInsets.top = readerViewNavBarHeight - // If the webview had been terminated `needsReload` will have been set to true - if context.coordinator.needsReload { + webView.configuration.userContentController.removeAllScriptMessageHandlers() + + for action in WebViewAction.allCases { + webView.configuration.userContentController.add(context.coordinator, name: action.rawValue) + } + + webView.configuration.userContentController.add(webView, name: "viewerAction") + + webView.configuration.userContentController.addScriptMessageHandler( + context.coordinator, contentWorld: .page, name: "articleAction" + ) + + context.coordinator.linkHandler = openLinkAction + context.coordinator.webViewActionHandler = webViewActionHandler + context.coordinator.updateNavBarVisibilityRatio = navBarVisibilityRatioUpdater loadContent(webView: webView) - context.coordinator.needsReload = false - return + + return webView } - if webView.isLoading { return } + func updateUIView(_ webView: WKWebView, context: Context) { + if annotationSaveTransactionID != context.coordinator.lastSavedAnnotationID { + context.coordinator.lastSavedAnnotationID = annotationSaveTransactionID + (webView as? WebView)?.saveAnnotation(annotation: annotation) + } - // If the root element is not detected then `WKWebView` may have unloaded the content - // so we need to load it again. - webView.evaluateJavaScript("document.getElementById('root') ? true : false") { hasRootElement, _ in - guard let hasRootElement = hasRootElement as? Bool else { return } + if increaseFontActionID != context.coordinator.previousIncreaseFontActionID { + context.coordinator.previousIncreaseFontActionID = increaseFontActionID + (webView as? WebView)?.increaseFontSize() + } - if !hasRootElement { - DispatchQueue.main.async { - loadContent(webView: webView) + if decreaseFontActionID != context.coordinator.previousDecreaseFontActionID { + context.coordinator.previousDecreaseFontActionID = decreaseFontActionID + (webView as? WebView)?.decreaseFontSize() + } + + // If the webview had been terminated `needsReload` will have been set to true + if context.coordinator.needsReload { + loadContent(webView: webView) + context.coordinator.needsReload = false + return + } + + if webView.isLoading { return } + + // If the root element is not detected then `WKWebView` may have unloaded the content + // so we need to load it again. + webView.evaluateJavaScript("document.getElementById('root') ? true : false") { hasRootElement, _ in + guard let hasRootElement = hasRootElement as? Bool else { return } + + if !hasRootElement { + DispatchQueue.main.async { + loadContent(webView: webView) + } } } } - } - func loadContent(webView: WKWebView) { - webView.loadHTMLString( - WebReaderContent( - articleContent: articleContent, - item: item, - isDark: UITraitCollection.current.userInterfaceStyle == .dark, - fontSize: fontSize() + func loadContent(webView: WKWebView) { + webView.loadHTMLString( + WebReaderContent( + articleContent: articleContent, + item: item, + isDark: UITraitCollection.current.userInterfaceStyle == .dark, + fontSize: fontSize() + ) + .styledContent, + baseURL: ViewsPackage.bundleURL ) - .styledContent, - baseURL: ViewsPackage.bundleURL - ) + } } -} +#endif diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 51bc8fca4..4759c527b 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -5,278 +5,280 @@ import SwiftUI import Views import WebKit -struct WebReaderContainerView: View { - let item: FeedItem - let homeFeedViewModel: HomeFeedViewModel +#if os(iOS) + struct WebReaderContainerView: View { + let item: FeedItem + let homeFeedViewModel: HomeFeedViewModel - @State private var showFontSizePopover = false - @State var showHighlightAnnotationModal = false - @State var safariWebLink: SafariWebLink? - @State private var navBarVisibilityRatio = 1.0 - @State private var showDeleteConfirmation = false - @State private var showOverlay = true - @State var increaseFontActionID: UUID? - @State var decreaseFontActionID: UUID? - @State var annotationSaveTransactionID: UUID? - @State var annotation = String() + @State private var showFontSizePopover = false + @State var showHighlightAnnotationModal = false + @State var safariWebLink: SafariWebLink? + @State private var navBarVisibilityRatio = 1.0 + @State private var showDeleteConfirmation = false + @State private var showOverlay = true + @State var increaseFontActionID: UUID? + @State var decreaseFontActionID: UUID? + @State var annotationSaveTransactionID: UUID? + @State var annotation = String() - @EnvironmentObject var dataService: DataService - @Environment(\.presentationMode) var presentationMode: Binding - @StateObject var viewModel = WebReaderViewModel() + @EnvironmentObject var dataService: DataService + @Environment(\.presentationMode) var presentationMode: Binding + @StateObject var viewModel = WebReaderViewModel() - var fontAdjustmentPopoverView: some View { - FontSizeAdjustmentPopoverView( - increaseFontAction: { increaseFontActionID = UUID() }, - decreaseFontAction: { decreaseFontActionID = UUID() } - ) - } + var fontAdjustmentPopoverView: some View { + FontSizeAdjustmentPopoverView( + increaseFontAction: { increaseFontActionID = UUID() }, + decreaseFontAction: { decreaseFontActionID = UUID() } + ) + } - func webViewActionHandler(message: WKScriptMessage, replyHandler: WKScriptMessageReplyHandler?) { - if message.name == WebViewAction.readingProgressUpdate.rawValue { - let messageBody = message.body as? [String: Double] + func webViewActionHandler(message: WKScriptMessage, replyHandler: WKScriptMessageReplyHandler?) { + if message.name == WebViewAction.readingProgressUpdate.rawValue { + let messageBody = message.body as? [String: Double] - if let messageBody = messageBody, let progress = messageBody["progress"] { + if let messageBody = messageBody, let progress = messageBody["progress"] { + homeFeedViewModel.uncommittedReadingProgressUpdates[item.id] = Double(progress) + } + } + + if let replyHandler = replyHandler { + viewModel.webViewActionWithReplyHandler( + message: message, + replyHandler: replyHandler, + dataService: dataService + ) + return + } + + if message.name == WebViewAction.highlightAction.rawValue { + handleHighlightAction(message: message) + } + + if message.name == WebViewAction.readingProgressUpdate.rawValue { + guard let messageBody = message.body as? [String: Double] else { return } + guard let progress = messageBody["progress"] else { return } homeFeedViewModel.uncommittedReadingProgressUpdates[item.id] = Double(progress) } } - if let replyHandler = replyHandler { - viewModel.webViewActionWithReplyHandler( - message: message, - replyHandler: replyHandler, - dataService: dataService - ) - return - } + private func handleHighlightAction(message: WKScriptMessage) { + guard let messageBody = message.body as? [String: String] else { return } + guard let actionID = messageBody["actionID"] else { return } - if message.name == WebViewAction.highlightAction.rawValue { - handleHighlightAction(message: message) - } - - if message.name == WebViewAction.readingProgressUpdate.rawValue { - guard let messageBody = message.body as? [String: Double] else { return } - guard let progress = messageBody["progress"] else { return } - homeFeedViewModel.uncommittedReadingProgressUpdates[item.id] = Double(progress) - } - } - - private func handleHighlightAction(message: WKScriptMessage) { - guard let messageBody = message.body as? [String: String] else { return } - guard let actionID = messageBody["actionID"] else { return } - - switch actionID { - case "annotate": - annotation = messageBody["annotation"] ?? "" - showHighlightAnnotationModal = true - default: - break - } - } - - var navBariOS14: some View { - HStack(alignment: .center) { - Button( - action: { self.presentationMode.wrappedValue.dismiss() }, - label: { - Image(systemName: "chevron.backward") - .font(.appTitleTwo) - .foregroundColor(.appGrayTextContrast) - .padding(.horizontal) - } - ) - .scaleEffect(navBarVisibilityRatio) - Spacer() - Button( - action: { showFontSizePopover.toggle() }, - label: { - Image(systemName: "textformat.size") - .font(.appTitleTwo) - } - ) - .padding(.horizontal) - .scaleEffect(navBarVisibilityRatio) - } - .frame(height: readerViewNavBarHeight * navBarVisibilityRatio) - .opacity(navBarVisibilityRatio) - .background(Color.systemBackground) - .onTapGesture { - showFontSizePopover = false - } - } - - @available(macOS 12.0, *) - @available(iOS 15.0, *) - var navBar: some View { - HStack(alignment: .center) { - Button( - action: { self.presentationMode.wrappedValue.dismiss() }, - label: { - Image(systemName: "chevron.backward") - .font(.appTitleTwo) - .foregroundColor(.appGrayTextContrast) - .padding(.horizontal) - } - ) - .scaleEffect(navBarVisibilityRatio) - Spacer() - Button( - action: { showFontSizePopover.toggle() }, - label: { - Image(systemName: "textformat.size") - .font(.appTitleTwo) - } - ) - .padding(.horizontal) - .scaleEffect(navBarVisibilityRatio) - Menu( - content: { - Group { - Button( - action: { - homeFeedViewModel.setLinkArchived( - dataService: dataService, - linkId: item.id, - archived: !item.isArchived - ) - }, - label: { - Label( - item.isArchived ? "Unarchive" : "Archive", - systemImage: item.isArchived ? "tray.and.arrow.down.fill" : "archivebox" - ) - } - ) - Button( - action: { showDeleteConfirmation = true }, - label: { Label("Delete", systemImage: "trash") } - ) - } - }, - label: { - Image.profile - .padding(.horizontal) - .scaleEffect(navBarVisibilityRatio) - } - ) - } - .frame(height: readerViewNavBarHeight * navBarVisibilityRatio) - .opacity(navBarVisibilityRatio) - .background(Color.systemBackground) - .onTapGesture { - showFontSizePopover = false - } - .alert("Are you sure?", isPresented: $showDeleteConfirmation) { - Button("Remove Link", role: .destructive) { - homeFeedViewModel.removeLink(dataService: dataService, linkId: item.id) + switch actionID { + case "annotate": + annotation = messageBody["annotation"] ?? "" + showHighlightAnnotationModal = true + default: + break } - Button("Cancel", role: .cancel, action: {}) } - } - var body: some View { - ZStack { - if let articleContent = viewModel.articleContent { - WebReader( - articleContent: articleContent, - item: item, - openLinkAction: { - #if os(macOS) - NSWorkspace.shared.open($0) - #elseif os(iOS) - safariWebLink = SafariWebLink(id: UUID(), url: $0) - #endif - }, - webViewActionHandler: webViewActionHandler, - navBarVisibilityRatioUpdater: { - if $0 < 1 { - showFontSizePopover = false - } - navBarVisibilityRatio = $0 - }, - increaseFontActionID: $increaseFontActionID, - decreaseFontActionID: $decreaseFontActionID, - annotationSaveTransactionID: $annotationSaveTransactionID, - annotation: $annotation + var navBariOS14: some View { + HStack(alignment: .center) { + Button( + action: { self.presentationMode.wrappedValue.dismiss() }, + label: { + Image(systemName: "chevron.backward") + .font(.appTitleTwo) + .foregroundColor(.appGrayTextContrast) + .padding(.horizontal) + } ) - .overlay( - Group { - if showOverlay { - Color.systemBackground - .transition(.opacity) - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { - withAnimation(.linear(duration: 0.2)) { - showOverlay = false + .scaleEffect(navBarVisibilityRatio) + Spacer() + Button( + action: { showFontSizePopover.toggle() }, + label: { + Image(systemName: "textformat.size") + .font(.appTitleTwo) + } + ) + .padding(.horizontal) + .scaleEffect(navBarVisibilityRatio) + } + .frame(height: readerViewNavBarHeight * navBarVisibilityRatio) + .opacity(navBarVisibilityRatio) + .background(Color.systemBackground) + .onTapGesture { + showFontSizePopover = false + } + } + + @available(macOS 12.0, *) + @available(iOS 15.0, *) + var navBar: some View { + HStack(alignment: .center) { + Button( + action: { self.presentationMode.wrappedValue.dismiss() }, + label: { + Image(systemName: "chevron.backward") + .font(.appTitleTwo) + .foregroundColor(.appGrayTextContrast) + .padding(.horizontal) + } + ) + .scaleEffect(navBarVisibilityRatio) + Spacer() + Button( + action: { showFontSizePopover.toggle() }, + label: { + Image(systemName: "textformat.size") + .font(.appTitleTwo) + } + ) + .padding(.horizontal) + .scaleEffect(navBarVisibilityRatio) + Menu( + content: { + Group { + Button( + action: { + homeFeedViewModel.setLinkArchived( + dataService: dataService, + linkId: item.id, + archived: !item.isArchived + ) + }, + label: { + Label( + item.isArchived ? "Unarchive" : "Archive", + systemImage: item.isArchived ? "tray.and.arrow.down.fill" : "archivebox" + ) + } + ) + Button( + action: { showDeleteConfirmation = true }, + label: { Label("Delete", systemImage: "trash") } + ) + } + }, + label: { + Image.profile + .padding(.horizontal) + .scaleEffect(navBarVisibilityRatio) + } + ) + } + .frame(height: readerViewNavBarHeight * navBarVisibilityRatio) + .opacity(navBarVisibilityRatio) + .background(Color.systemBackground) + .onTapGesture { + showFontSizePopover = false + } + .alert("Are you sure?", isPresented: $showDeleteConfirmation) { + Button("Remove Link", role: .destructive) { + homeFeedViewModel.removeLink(dataService: dataService, linkId: item.id) + } + Button("Cancel", role: .cancel, action: {}) + } + } + + var body: some View { + ZStack { + if let articleContent = viewModel.articleContent { + WebReader( + articleContent: articleContent, + item: item, + openLinkAction: { + #if os(macOS) + NSWorkspace.shared.open($0) + #elseif os(iOS) + safariWebLink = SafariWebLink(id: UUID(), url: $0) + #endif + }, + webViewActionHandler: webViewActionHandler, + navBarVisibilityRatioUpdater: { + if $0 < 1 { + showFontSizePopover = false + } + navBarVisibilityRatio = $0 + }, + increaseFontActionID: $increaseFontActionID, + decreaseFontActionID: $decreaseFontActionID, + annotationSaveTransactionID: $annotationSaveTransactionID, + annotation: $annotation + ) + .overlay( + Group { + if showOverlay { + Color.systemBackground + .transition(.opacity) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { + withAnimation(.linear(duration: 0.2)) { + showOverlay = false + } } } - } - } - } - ) - .sheet(item: $safariWebLink) { - SafariView(url: $0.url) - } - .sheet(isPresented: $showHighlightAnnotationModal) { - HighlightAnnotationSheet( - annotation: $annotation, - onSave: { - annotationSaveTransactionID = UUID() - showHighlightAnnotationModal = false - }, - onCancel: { - showHighlightAnnotationModal = false + } } ) - } - } else { - Color.clear - .contentShape(Rectangle()) - .onAppear { - if !viewModel.isLoading { - viewModel.loadContent(dataService: dataService, slug: item.slug) - } + .sheet(item: $safariWebLink) { + SafariView(url: $0.url) } - } - if showFontSizePopover { - VStack { + .sheet(isPresented: $showHighlightAnnotationModal) { + HighlightAnnotationSheet( + annotation: $annotation, + onSave: { + annotationSaveTransactionID = UUID() + showHighlightAnnotationModal = false + }, + onCancel: { + showHighlightAnnotationModal = false + } + ) + } + } else { Color.clear .contentShape(Rectangle()) - .frame(height: LinkItemDetailView.navBarHeight) - HStack { + .onAppear { + if !viewModel.isLoading { + viewModel.loadContent(dataService: dataService, slug: item.slug) + } + } + } + if showFontSizePopover { + VStack { + Color.clear + .contentShape(Rectangle()) + .frame(height: LinkItemDetailView.navBarHeight) + HStack { + Spacer() + fontAdjustmentPopoverView + .background(Color.appButtonBackground) + .cornerRadius(8) + .padding(.trailing, 44) + } Spacer() - fontAdjustmentPopoverView - .background(Color.appButtonBackground) - .cornerRadius(8) - .padding(.trailing, 44) } - Spacer() + .background( + Color.clear + .contentShape(Rectangle()) + .onTapGesture { + showFontSizePopover = false + } + ) } - .background( - Color.clear - .contentShape(Rectangle()) - .onTapGesture { - showFontSizePopover = false - } - ) - } - if #available(iOS 15.0, *) { - VStack(spacing: 0) { - navBar - Spacer() + if #available(iOS 15.0, *) { + VStack(spacing: 0) { + navBar + Spacer() + } + .navigationBarHidden(true) + } else { + VStack(spacing: 0) { + navBariOS14 + Spacer() + } + .navigationBarHidden(true) } - .navigationBarHidden(true) - } else { - VStack(spacing: 0) { - navBariOS14 - Spacer() - } - .navigationBarHidden(true) - } - }.onDisappear { - // Clear the shared webview content when exiting - WebViewManager.shared().loadHTMLString("", baseURL: nil) + }.onDisappear { + // Clear the shared webview content when exiting + WebViewManager.shared().loadHTMLString("", baseURL: nil) + } + .navigationBarHidden(true) } - .navigationBarHidden(true) } -} +#endif diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift index c75bca135..54f7edb5b 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift @@ -2,7 +2,9 @@ import Combine import Models import Services import SwiftUI -import UIKit +#if os(iOS) + import UIKit +#endif import Utils import Views import WebKit diff --git a/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift b/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift index 2c20306f6..a676666c5 100644 --- a/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift +++ b/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift @@ -45,9 +45,17 @@ public extension Color { } private func toHex() -> String? { - guard let components = UIColor(self).cgColor.components, components.count >= 3 else { - return nil - } + #if os(iOS) + guard let components = UIColor(self).cgColor.components, components.count >= 3 else { + return nil + } + #endif + + #if os(macOS) + guard let components = NSColor(self).cgColor.components, components.count >= 3 else { + return nil + } + #endif let red = Float(components[0]) let green = Float(components[1]) let blue = Float(components[2]) @@ -61,9 +69,17 @@ public extension Color { } var isDark: Bool { - guard let components = UIColor(self).cgColor.components, components.count >= 3 else { - return false - } + #if os(iOS) + guard let components = UIColor(self).cgColor.components, components.count >= 3 else { + return false + } + #endif + + #if os(macOS) + guard let components = NSColor(self).cgColor.components, components.count >= 3 else { + return false + } + #endif let lum = 0.2126 * Float(components[0]) + 0.7152 * Float(components[1]) + 0.0722 * Float(components[2]) return lum < 0.50 diff --git a/apple/OmnivoreKit/Sources/Views/Article/WebAppView.swift b/apple/OmnivoreKit/Sources/Views/Article/WebAppView.swift index 07e458365..a78344f6e 100644 --- a/apple/OmnivoreKit/Sources/Views/Article/WebAppView.swift +++ b/apple/OmnivoreKit/Sources/Views/Article/WebAppView.swift @@ -10,7 +10,9 @@ enum WebViewConfigurationManager { static func create() -> WKWebViewConfiguration { let config = WKWebViewConfiguration() config.processPool = processPool - config.allowsInlineMediaPlayback = true + #if os(iOS) + config.allowsInlineMediaPlayback = true + #endif config.mediaTypesRequiringUserActionForPlayback = .audio return config } diff --git a/apple/OmnivoreKit/Sources/Views/Article/WebView.swift b/apple/OmnivoreKit/Sources/Views/Article/WebView.swift index 9744d2f9a..5a6277f00 100644 --- a/apple/OmnivoreKit/Sources/Views/Article/WebView.swift +++ b/apple/OmnivoreKit/Sources/Views/Article/WebView.swift @@ -55,7 +55,7 @@ public final class WebView: WKWebView { } #elseif os(macOS) - override func viewDidChangeEffectiveAppearance() { + override public func viewDidChangeEffectiveAppearance() { super.viewDidChangeEffectiveAppearance() switch effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) { case .some(.darkAqua): From 2ebe340509eb49ab0bfcd68367f64e4f128a7004 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 8 Apr 2022 08:47:40 -0700 Subject: [PATCH 11/19] reference the correct searchQuery var in macapp --- .../Sources/App/Views/Home/HomeFeedViewMac.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift index caed1c399..de7ac9a1d 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift @@ -9,7 +9,6 @@ import Views #if os(macOS) struct HomeFeedView: View { @EnvironmentObject var dataService: DataService - @State var searchQuery = "" @State private var selectedLinkItem: FeedItem? @State private var itemToRemove: FeedItem? @State private var confirmationShown = false @@ -82,17 +81,17 @@ import Views .listStyle(PlainListStyle()) .navigationTitle("Home") .searchable( - text: $searchQuery, + text: $viewModel.searchQuery, placement: .toolbar ) { - if searchQuery.isEmpty { + if viewModel.searchQuery.isEmpty { Text("Inbox").searchCompletion("in:inbox ") Text("All").searchCompletion("in:all ") Text("Archived").searchCompletion("in:archive ") Text("Files").searchCompletion("type:file ") } } - .onChange(of: searchQuery) { _ in + .onChange(of: viewModel.searchQuery) { _ in // Maybe we should debounce this, but // it feels like it works ok without viewModel.loadItems(dataService: dataService, isRefresh: true) From 4c21bb4c69c24be2097544078c4b5098cb05f9a5 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 8 Apr 2022 09:10:51 -0700 Subject: [PATCH 12/19] move snoozePresented var into home View Model --- .../Sources/App/Views/Home/HomeFeedViewIOS.swift | 16 ++++------------ .../Sources/App/Views/Home/HomeFeedViewMac.swift | 3 +-- .../App/Views/Home/HomeFeedViewModel.swift | 1 + .../OmnivoreKit/Sources/Utils/FeatureFlags.swift | 2 +- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 1991ede19..33479cc75 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -10,7 +10,6 @@ import Views struct HomeFeedContainerView: View { @EnvironmentObject var dataService: DataService @AppStorage(UserDefaultKey.homeFeedlayoutPreference.rawValue) var prefersListLayout = UIDevice.isIPhone - @State private var snoozePresented = false @State private var itemToSnooze: FeedItem? @State private var selectedLinkItem: FeedItem? @ObservedObject var viewModel: HomeFeedViewModel @@ -21,7 +20,6 @@ import Views HomeFeedView( prefersListLayout: $prefersListLayout, selectedLinkItem: $selectedLinkItem, - snoozePresented: $snoozePresented, itemToSnooze: $itemToSnooze, viewModel: viewModel ) @@ -56,7 +54,6 @@ import Views HomeFeedView( prefersListLayout: $prefersListLayout, selectedLinkItem: $selectedLinkItem, - snoozePresented: $snoozePresented, itemToSnooze: $itemToSnooze, viewModel: viewModel ) @@ -92,8 +89,8 @@ import Views self.selectedLinkItem = feedItem } } - .formSheet(isPresented: $snoozePresented) { - SnoozeView(snoozePresented: $snoozePresented, itemToSnooze: $itemToSnooze) { + .formSheet(isPresented: $viewModel.snoozePresented) { + SnoozeView(snoozePresented: $viewModel.snoozePresented, itemToSnooze: $itemToSnooze) { viewModel.snoozeUntil( dataService: dataService, linkId: $0.feedItemId, @@ -118,7 +115,6 @@ import Views @Binding var prefersListLayout: Bool @Binding var selectedLinkItem: FeedItem? - @Binding var snoozePresented: Bool @Binding var itemToSnooze: FeedItem? @ObservedObject var viewModel: HomeFeedViewModel @@ -128,14 +124,12 @@ import Views HomeFeedListView( prefersListLayout: $prefersListLayout, selectedLinkItem: $selectedLinkItem, - snoozePresented: $snoozePresented, itemToSnooze: $itemToSnooze, viewModel: viewModel ) } else { HomeFeedGridView( selectedLinkItem: $selectedLinkItem, - snoozePresented: $snoozePresented, itemToSnooze: $itemToSnooze, viewModel: viewModel ) @@ -179,7 +173,6 @@ import Views @EnvironmentObject var dataService: DataService @Binding var prefersListLayout: Bool @Binding var selectedLinkItem: FeedItem? - @Binding var snoozePresented: Bool @Binding var itemToSnooze: FeedItem? @State private var itemToRemove: FeedItem? @@ -217,7 +210,7 @@ import Views if FeatureFlag.enableSnooze { Button { itemToSnooze = item - snoozePresented = true + viewModel.snoozePresented = true } label: { Label { Text("Snooze") } icon: { Image.moon } } @@ -270,7 +263,7 @@ import Views if FeatureFlag.enableSnooze { Button { itemToSnooze = item - snoozePresented = true + viewModel.snoozePresented = true } label: { Label { Text("Snooze") } icon: { Image.moon } }.tint(.appYellow48) @@ -308,7 +301,6 @@ import Views struct HomeFeedGridView: View { @EnvironmentObject var dataService: DataService @Binding var selectedLinkItem: FeedItem? - @Binding var snoozePresented: Bool @Binding var itemToSnooze: FeedItem? @State private var itemToRemove: FeedItem? diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift index de7ac9a1d..a45956258 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift @@ -12,7 +12,6 @@ import Views @State private var selectedLinkItem: FeedItem? @State private var itemToRemove: FeedItem? @State private var confirmationShown = false - @State private var snoozePresented = false @State private var itemToSnooze: FeedItem? @ObservedObject var viewModel: HomeFeedViewModel @@ -54,7 +53,7 @@ import Views if FeatureFlag.enableSnooze { Button { itemToSnooze = item - snoozePresented = true + viewModel.snoozePresented = true } label: { Label { Text("Snooze") } icon: { Image.moon } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 18ef3b6e7..af451e827 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -16,6 +16,7 @@ final class HomeFeedViewModel: ObservableObject { @Published var showPushNotificationPrimer = false @Published var itemUnderLabelEdit: FeedItem? @Published var searchQuery = "" + @Published var snoozePresented = false var cursor: String? var sendProgressUpdates = false diff --git a/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift b/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift index ab5196f5a..01e306528 100644 --- a/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift +++ b/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift @@ -13,7 +13,7 @@ public enum FeatureFlag { public static let enableRemindersFromShareExtension = false public static let enablePushNotifications = false public static let enableShareButton = false - public static let enableSnooze = false + public static let enableSnooze = true public static let enableLabels = true public static let useLocalWebView = true } From b8ac528b2a1db69cb45e2c5654d7799b41e689de Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 8 Apr 2022 09:21:42 -0700 Subject: [PATCH 13/19] move itemToSnooze var into HomeViewModel --- .../Sources/App/Views/Home/HomeFeedViewIOS.swift | 14 +++----------- .../Sources/App/Views/Home/HomeFeedViewMac.swift | 3 +-- .../Sources/App/Views/Home/HomeFeedViewModel.swift | 1 + 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 33479cc75..deb0f7cdc 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -10,7 +10,6 @@ import Views struct HomeFeedContainerView: View { @EnvironmentObject var dataService: DataService @AppStorage(UserDefaultKey.homeFeedlayoutPreference.rawValue) var prefersListLayout = UIDevice.isIPhone - @State private var itemToSnooze: FeedItem? @State private var selectedLinkItem: FeedItem? @ObservedObject var viewModel: HomeFeedViewModel @@ -20,7 +19,6 @@ import Views HomeFeedView( prefersListLayout: $prefersListLayout, selectedLinkItem: $selectedLinkItem, - itemToSnooze: $itemToSnooze, viewModel: viewModel ) .refreshable { @@ -54,7 +52,6 @@ import Views HomeFeedView( prefersListLayout: $prefersListLayout, selectedLinkItem: $selectedLinkItem, - itemToSnooze: $itemToSnooze, viewModel: viewModel ) .sheet(item: $viewModel.itemUnderLabelEdit) { item in @@ -90,7 +87,7 @@ import Views } } .formSheet(isPresented: $viewModel.snoozePresented) { - SnoozeView(snoozePresented: $viewModel.snoozePresented, itemToSnooze: $itemToSnooze) { + SnoozeView(snoozePresented: $viewModel.snoozePresented, itemToSnooze: $viewModel.itemToSnooze) { viewModel.snoozeUntil( dataService: dataService, linkId: $0.feedItemId, @@ -115,7 +112,6 @@ import Views @Binding var prefersListLayout: Bool @Binding var selectedLinkItem: FeedItem? - @Binding var itemToSnooze: FeedItem? @ObservedObject var viewModel: HomeFeedViewModel @@ -124,13 +120,11 @@ import Views HomeFeedListView( prefersListLayout: $prefersListLayout, selectedLinkItem: $selectedLinkItem, - itemToSnooze: $itemToSnooze, viewModel: viewModel ) } else { HomeFeedGridView( selectedLinkItem: $selectedLinkItem, - itemToSnooze: $itemToSnooze, viewModel: viewModel ) .toolbar { @@ -173,7 +167,6 @@ import Views @EnvironmentObject var dataService: DataService @Binding var prefersListLayout: Bool @Binding var selectedLinkItem: FeedItem? - @Binding var itemToSnooze: FeedItem? @State private var itemToRemove: FeedItem? @State private var confirmationShown = false @@ -209,7 +202,7 @@ import Views ) if FeatureFlag.enableSnooze { Button { - itemToSnooze = item + viewModel.itemToSnooze = item viewModel.snoozePresented = true } label: { Label { Text("Snooze") } icon: { Image.moon } @@ -262,7 +255,7 @@ import Views .swipeActions(edge: .leading, allowsFullSwipe: true) { if FeatureFlag.enableSnooze { Button { - itemToSnooze = item + viewModel.itemToSnooze = item viewModel.snoozePresented = true } label: { Label { Text("Snooze") } icon: { Image.moon } @@ -301,7 +294,6 @@ import Views struct HomeFeedGridView: View { @EnvironmentObject var dataService: DataService @Binding var selectedLinkItem: FeedItem? - @Binding var itemToSnooze: FeedItem? @State private var itemToRemove: FeedItem? @State private var confirmationShown = false diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift index a45956258..e0d39f2bd 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift @@ -12,7 +12,6 @@ import Views @State private var selectedLinkItem: FeedItem? @State private var itemToRemove: FeedItem? @State private var confirmationShown = false - @State private var itemToSnooze: FeedItem? @ObservedObject var viewModel: HomeFeedViewModel @@ -52,7 +51,7 @@ import Views ) if FeatureFlag.enableSnooze { Button { - itemToSnooze = item + viewModel.itemToSnooze = item viewModel.snoozePresented = true } label: { Label { Text("Snooze") } icon: { Image.moon } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index af451e827..8e8264f8c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -17,6 +17,7 @@ final class HomeFeedViewModel: ObservableObject { @Published var itemUnderLabelEdit: FeedItem? @Published var searchQuery = "" @Published var snoozePresented = false + @Published var itemToSnooze: FeedItem? var cursor: String? var sendProgressUpdates = false From b195414e1619694fb412cf8d6f980eaa4c1ac591 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 8 Apr 2022 09:30:33 -0700 Subject: [PATCH 14/19] track selectedLinkItem in HomeFeedViewModel --- .../Components/FeedCardNavigationLink.swift | 9 +-- .../App/Views/Home/HomeFeedViewIOS.swift | 75 ++++++++----------- .../App/Views/Home/HomeFeedViewMac.swift | 3 - .../App/Views/Home/HomeFeedViewModel.swift | 3 +- 4 files changed, 35 insertions(+), 55 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift index 1c19b5398..dbf0084ca 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift @@ -8,8 +8,6 @@ struct FeedCardNavigationLink: View { let item: FeedItem - @Binding var selectedLinkItem: FeedItem? - @ObservedObject var viewModel: HomeFeedViewModel var body: some View { @@ -17,7 +15,7 @@ struct FeedCardNavigationLink: View { NavigationLink( destination: LinkItemDetailView(viewModel: LinkItemDetailViewModel(item: item, homeFeedViewModel: viewModel)), tag: item, - selection: $selectedLinkItem + selection: $viewModel.selectedLinkItem ) { EmptyView() } @@ -39,7 +37,6 @@ struct GridCardNavigationLink: View { let item: FeedItem let actionHandler: (GridCardAction) -> Void - @Binding var selectedLinkItem: FeedItem? @Binding var isContextMenuOpen: Bool @ObservedObject var viewModel: HomeFeedViewModel @@ -49,7 +46,7 @@ struct GridCardNavigationLink: View { NavigationLink( destination: LinkItemDetailView(viewModel: LinkItemDetailViewModel(item: item, homeFeedViewModel: viewModel)), tag: item, - selection: $selectedLinkItem + selection: $viewModel.selectedLinkItem ) { EmptyView() } @@ -58,7 +55,7 @@ struct GridCardNavigationLink: View { scale = 0.95 DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(150)) { scale = 1.0 - selectedLinkItem = item + viewModel.selectedLinkItem = item } } }) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index deb0f7cdc..7c5e60d82 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -10,7 +10,6 @@ import Views struct HomeFeedContainerView: View { @EnvironmentObject var dataService: DataService @AppStorage(UserDefaultKey.homeFeedlayoutPreference.rawValue) var prefersListLayout = UIDevice.isIPhone - @State private var selectedLinkItem: FeedItem? @ObservedObject var viewModel: HomeFeedViewModel var body: some View { @@ -18,7 +17,6 @@ import Views if #available(iOS 15.0, *) { HomeFeedView( prefersListLayout: $prefersListLayout, - selectedLinkItem: $selectedLinkItem, viewModel: viewModel ) .refreshable { @@ -51,7 +49,6 @@ import Views } else { HomeFeedView( prefersListLayout: $prefersListLayout, - selectedLinkItem: $selectedLinkItem, viewModel: viewModel ) .sheet(item: $viewModel.itemUnderLabelEdit) { item in @@ -76,14 +73,14 @@ import Views .navigationTitle("Home") .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in // Don't refresh the list if the user is currently reading an article - if selectedLinkItem == nil { + if viewModel.selectedLinkItem == nil { viewModel.loadItems(dataService: dataService, isRefresh: true) } } .onReceive(NotificationCenter.default.publisher(for: Notification.Name("PushFeedItem"))) { notification in if let feedItem = notification.userInfo?["feedItem"] as? FeedItem { viewModel.pushFeedItem(item: feedItem) - self.selectedLinkItem = feedItem + viewModel.selectedLinkItem = feedItem } } .formSheet(isPresented: $viewModel.snoozePresented) { @@ -101,7 +98,7 @@ import Views viewModel.loadItems(dataService: dataService, isRefresh: true) } } - .onChange(of: selectedLinkItem) { _ in + .onChange(of: viewModel.selectedLinkItem) { _ in viewModel.commitProgressUpdates() } } @@ -111,54 +108,46 @@ import Views @EnvironmentObject var dataService: DataService @Binding var prefersListLayout: Bool - @Binding var selectedLinkItem: FeedItem? @ObservedObject var viewModel: HomeFeedViewModel var body: some View { if prefersListLayout { - HomeFeedListView( - prefersListLayout: $prefersListLayout, - selectedLinkItem: $selectedLinkItem, - viewModel: viewModel - ) + HomeFeedListView(prefersListLayout: $prefersListLayout, viewModel: viewModel) } else { - HomeFeedGridView( - selectedLinkItem: $selectedLinkItem, - viewModel: viewModel - ) - .toolbar { - ToolbarItem { - if #available(iOS 15.0, *) { - Button("", action: {}) - .disabled(true) - .overlay { - if viewModel.isLoading { - ProgressView() + HomeFeedGridView(viewModel: viewModel) + .toolbar { + ToolbarItem { + if #available(iOS 15.0, *) { + Button("", action: {}) + .disabled(true) + .overlay { + if viewModel.isLoading { + ProgressView() + } } - } - } else { - if viewModel.isLoading { - Button(action: {}, label: { ProgressView() }) } else { + if viewModel.isLoading { + Button(action: {}, label: { ProgressView() }) + } else { + Button( + action: { viewModel.loadItems(dataService: dataService, isRefresh: true) }, + label: { Label("Refresh Feed", systemImage: "arrow.clockwise") } + ) + } + } + } + ToolbarItem { + if UIDevice.isIPad { Button( - action: { viewModel.loadItems(dataService: dataService, isRefresh: true) }, - label: { Label("Refresh Feed", systemImage: "arrow.clockwise") } + action: { prefersListLayout.toggle() }, + label: { + Label("Toggle Feed Layout", systemImage: prefersListLayout ? "square.grid.2x2" : "list.bullet") + } ) } } } - ToolbarItem { - if UIDevice.isIPad { - Button( - action: { prefersListLayout.toggle() }, - label: { - Label("Toggle Feed Layout", systemImage: prefersListLayout ? "square.grid.2x2" : "list.bullet") - } - ) - } - } - } } } } @@ -166,7 +155,6 @@ import Views struct HomeFeedListView: View { @EnvironmentObject var dataService: DataService @Binding var prefersListLayout: Bool - @Binding var selectedLinkItem: FeedItem? @State private var itemToRemove: FeedItem? @State private var confirmationShown = false @@ -179,7 +167,6 @@ import Views ForEach(viewModel.items) { item in let link = FeedCardNavigationLink( item: item, - selectedLinkItem: $selectedLinkItem, viewModel: viewModel ) .contextMenu { @@ -293,7 +280,6 @@ import Views struct HomeFeedGridView: View { @EnvironmentObject var dataService: DataService - @Binding var selectedLinkItem: FeedItem? @State private var itemToRemove: FeedItem? @State private var confirmationShown = false @@ -320,7 +306,6 @@ import Views let link = GridCardNavigationLink( item: item, actionHandler: { contextMenuActionHandler(item: item, action: $0) }, - selectedLinkItem: $selectedLinkItem, isContextMenuOpen: $isContextMenuOpen, viewModel: viewModel ) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift index e0d39f2bd..91cad42e8 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift @@ -9,7 +9,6 @@ import Views #if os(macOS) struct HomeFeedView: View { @EnvironmentObject var dataService: DataService - @State private var selectedLinkItem: FeedItem? @State private var itemToRemove: FeedItem? @State private var confirmationShown = false @@ -30,7 +29,6 @@ import Views ForEach(viewModel.items) { item in FeedCardNavigationLink( item: item, - selectedLinkItem: $selectedLinkItem, viewModel: viewModel ) .contextMenu { @@ -127,7 +125,6 @@ import Views ForEach(viewModel.items) { item in FeedCardNavigationLink( item: item, - selectedLinkItem: $selectedLinkItem, viewModel: viewModel ) } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 8e8264f8c..8a34b30cc 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -17,7 +17,8 @@ final class HomeFeedViewModel: ObservableObject { @Published var itemUnderLabelEdit: FeedItem? @Published var searchQuery = "" @Published var snoozePresented = false - @Published var itemToSnooze: FeedItem? + @Published var itemToSnooze: FeedItem? // TODO: maybe combine itemToSnooze and snoozePresented? + @Published var selectedLinkItem: FeedItem? var cursor: String? var sendProgressUpdates = false From a7b33942e78c74f62062de02a6f0b22877d88d14 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 8 Apr 2022 09:35:04 -0700 Subject: [PATCH 15/19] disable snooze --- apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift b/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift index 01e306528..ab5196f5a 100644 --- a/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift +++ b/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift @@ -13,7 +13,7 @@ public enum FeatureFlag { public static let enableRemindersFromShareExtension = false public static let enablePushNotifications = false public static let enableShareButton = false - public static let enableSnooze = true + public static let enableSnooze = false public static let enableLabels = true public static let useLocalWebView = true } From d5efebf25d2282cd3a505413657e70ccd8a6ec4f Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 8 Apr 2022 09:41:32 -0700 Subject: [PATCH 16/19] remove TODO comment --- .../OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 8a34b30cc..7a9af7d98 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -17,7 +17,7 @@ final class HomeFeedViewModel: ObservableObject { @Published var itemUnderLabelEdit: FeedItem? @Published var searchQuery = "" @Published var snoozePresented = false - @Published var itemToSnooze: FeedItem? // TODO: maybe combine itemToSnooze and snoozePresented? + @Published var itemToSnooze: FeedItem? @Published var selectedLinkItem: FeedItem? var cursor: String? From 0e0a9912d07a0cf5c37344b5fc81a869b99c3805 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 9 Apr 2022 14:40:48 +0000 Subject: [PATCH 17/19] Bump moment from 2.29.1 to 2.29.2 in /pkg/extension Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.2. - [Release notes](https://github.com/moment/moment/releases) - [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md) - [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.2) --- updated-dependencies: - dependency-name: moment dependency-type: indirect ... Signed-off-by: dependabot[bot] --- pkg/extension/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/extension/yarn.lock b/pkg/extension/yarn.lock index 217dc59b8..2426cd2ad 100644 --- a/pkg/extension/yarn.lock +++ b/pkg/extension/yarn.lock @@ -3108,9 +3108,9 @@ mkdirp@~0.5.1: minimist "^1.2.6" moment@^2.19.3: - version "2.29.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" - integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + version "2.29.2" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4" + integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg== ms@2.0.0: version "2.0.0" From b4420839bd121e9c82c71b28b5b77403685c8d06 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Mon, 11 Apr 2022 14:05:21 +0800 Subject: [PATCH 18/19] fix: fail to search by excluding labels with capital letter (#400) * fix: fail to search by excluding labels with capital letter * make all the excluded labels lowcased * Revert "make all the excluded labels lowcased" This reverts commit 866bed40801af522cb7f07aa3119ed813f7c34b6. --- packages/api/src/utils/search.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/api/src/utils/search.ts b/packages/api/src/utils/search.ts index 4efbd3045..b0f914607 100644 --- a/packages/api/src/utils/search.ts +++ b/packages/api/src/utils/search.ts @@ -98,18 +98,15 @@ const parseLabelFilter = ( return undefined } - // use lower case for label names - const labels = str.toLocaleLowerCase().split(',') + const labels = str.split(',') - // check if the labels are in the exclude list - const excluded = - exclude && - exclude.label && - labels.every((label) => exclude.label.includes(label)) + // check if the labels are on the exclusion list + const excluded = exclude?.label && exclude.label.includes(...labels) return { type: excluded ? LabelFilterType.EXCLUDE : LabelFilterType.INCLUDE, - labels, + // use lower case for label names + labels: labels.map((label) => label.toLowerCase()), } } From 4d01f689b2df3725ff5bc7dd5d2ae9ae0246bc36 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Mon, 11 Apr 2022 20:00:11 +0800 Subject: [PATCH 19/19] replace tables of article content with divs for newsletters --- packages/readabilityjs/Readability.js | 3 + .../milkroad/expected-metadata.json | 11 + .../test/test-pages/milkroad/expected.html | 282 ++++++ .../test/test-pages/milkroad/source.html | 958 ++++++++++++++++++ .../test/test-pages/milkroad/url.txt | 1 + 5 files changed, 1255 insertions(+) create mode 100644 packages/readabilityjs/test/test-pages/milkroad/expected-metadata.json create mode 100644 packages/readabilityjs/test/test-pages/milkroad/expected.html create mode 100644 packages/readabilityjs/test/test-pages/milkroad/source.html create mode 100644 packages/readabilityjs/test/test-pages/milkroad/url.txt diff --git a/packages/readabilityjs/Readability.js b/packages/readabilityjs/Readability.js index f91dc4dc6..36d09102c 100644 --- a/packages/readabilityjs/Readability.js +++ b/packages/readabilityjs/Readability.js @@ -898,6 +898,9 @@ Readability.prototype = { } }); + // replace tables of article content with divs for newsletters + this._keepTables && this._replaceNodeTags(this._getAllNodesWithTag(articleContent, ["table"]), "div"); + // Final clean up of nodes that might pass readability conditions but still contain redundant text // For example, this article (https://www.sciencedirect.com/science/article/abs/pii/S0047248498902196) // has a "View full text" anchor at the bottom of the page diff --git a/packages/readabilityjs/test/test-pages/milkroad/expected-metadata.json b/packages/readabilityjs/test/test-pages/milkroad/expected-metadata.json new file mode 100644 index 000000000..16ef00bd5 --- /dev/null +++ b/packages/readabilityjs/test/test-pages/milkroad/expected-metadata.json @@ -0,0 +1,11 @@ +{ + "title": "🥛 Peter Thiel Calls Warren Buffett a Sociopathic Grandpa from Omaha", + "byline": null, + "dir": null, + "excerpt": "The newsletter that makes you smarter about web3", + "siteName": null, + "siteIcon": "https://media.beehiiv.net/uploads/publication/logo/654e9594-184c-4884-8e02-e6e58a3a6871/thumb_Untitled__1000_x_1000_px___2_.png", + "previewImage": "https://media.beehiiv.net/uploads/asset/file/30564/Screenshot_2022-04-08_115750.png", + "publishedDate": null, + "readerable": true +} diff --git a/packages/readabilityjs/test/test-pages/milkroad/expected.html b/packages/readabilityjs/test/test-pages/milkroad/expected.html new file mode 100644 index 000000000..8fd76a641 --- /dev/null +++ b/packages/readabilityjs/test/test-pages/milkroad/expected.html @@ -0,0 +1,282 @@ +
+
+ +
+
+

+ By Shaan Puri & Ben Levy +

+

+

+
+
+

+

+

+ GM. This is the Milk Road, we cut the crypto sandwich into triangles, just the way you like it.  +

+

+ Today's estimated read time: 4 minutes & 7 seconds +

+

We’re skipping price action today, because we have a LOT of juicy stuff to go over. 

+

Juicy? What happened?

+

Day 2 of The Bitcoin Conference was fire, so I watched all the talks and summarized all the important stuff for you.

+

+

+

+ What happened:  +

+

Peter Thiel (co–founder of Paypal, and first investor in Facebook) got on stage and gave a crazy talk. 

+

+ I kept a running diary of my thoughts during his talk +

+

+ 0:00 - Wow, they got Peter Thiel to talk. *gets pen & notepad out* +

+

+ 0:04 - They start by playing a video clip of Peter talking at a conference in 1999. He’s making 2 predictions:  +

+
+
    +
  1. It’s 1999, but he’s predicting that 1B people will have cell phones connected to the internet in 5 years (he’s right, but it took about 10 years instead of 5)
  2. +
  3. People will have money on their cell phone. It will be a currency that isn’t controlled by their local government (sounds like crypto, without saying crypto) 
  4. +
+
+

+ 2:40 - OK, the real Peter is on stage now. Crowd cheers. They sound a little tipsy even though it’s 11am.. Nice. This is why we do conferences in Miami.  +

+

He starts off with a bold move. He’s waving a stack of $100 bills in the air.

+

He asks: “what is money?,” “this always gets people's attention,” “it’s not good as toilet paper. It’s not wallpaper. But people want it. Do you want it? Come get it!”

+

+

+

+ 3:16 - He throws the wad of cash at someone in the crowd then laughs as they fight over it: “I thought you guys were supposed to be Bitcon maximalists.” The joke lands. +

+

+ 3:30 - He talks about the early days of PayPal. Shows a picture of him and Elon Musk (they were both CEOs of Paypal early on). Pretty wild to think about how much talent was in 1 startup.  +

+

Here’s who worked at Paypal: 

+
+
    +
  • Peter - created PayPal, first investor in Facebook
  • +
  • Elon - CEO of Tesla, SpaceX
  • +
  • The founders of YouTube
  • +
  • The founder of LinkedIn
  • +
  • The founder of Yelp 
  • +
  • The founder of Kiva.org 
  • +
  • The founder of Yammer
  • +
  • The COO of Square
  • +
  • The future CEO of Reddit
  • +
+
+

This is called the PayPal mafia^. What a killer roster of talent. OK, back to Peter’s talk: 

+

+ 3:55 - He shows a slide from the first pitch deck of PayPal. The initial idea was a lot like Bitcoin. A currency that lived outside of the banking system. He said “we didn’t know anything” back then, but they had big ambitions. +

+

+

+

+ 4:12 - But that was really hard. So within a couple years, they had created PayPal. Which worked with the banking system instead of replacing it. This was more practical, but less ambitious. It was just a payment system, not a new kind of money.  +

+

+ 6:49 - Payment system vs. Money. Why does it matter?  +

+

Peter points out an important thing about money. Velocity. 

+

The simple idea is that people will spend their “less valuable” money first. Back in the day, people would hoard gold, and spend silver. 

+

Today, people will save Bitcoin, and spend dollars.

+

This means some money moves “fast” throughout the economy (hopping from person to person). To most people, that makes a currency more valuable! It’s being used! Must be more valuable, right?

+

The velocity theory says that’s wrong. The slower moving money will end up being higher priced. Nobody is ‘holding’ the fast money, so it doesn’t go up in price (sellers willing to sell). 

+

Anyways, Peter’s belief is that Bitcoin is “slow money” and therefore, it’s valuable money. He says Ethereum is a payments network, high velocity, and lower value.

+

+

+

If Peter’s right, Bitcoin will end up a lot more valuable than ETH over time. (Editor’s note: I totally disagree. I think he’s wrong in thinking ETH is a payments network)

+

+ 7:36 - OK, this is where the talk starts getting aggressive.  +

+

He puts up this slide. The crowd goes wild. Crypto is so tribal.

+

He is trying to talk but you can barely hear him, because the red-blooded-american crowd is going wild for no reason.

+

+

+

+ 8:13 - He says Bitcoin is like gold (a store of value). And ETH is like Visa (a payments network).  +

+

He points out that Gold has a total market value of $12T, and the biggest payment network (Visa) is worth 26x less. 

+

If Peter is right, Bitcoin has more upside than ETH (again, I disagree,but let’s let Peter talk. He’s the billionaire chess grandmaster. I’m just a hairy dude with a free newsletter)

+

+ 11:26 - He puts up this slide showing that back in the day (1980) Gold was equal to the total value of all stocks. (Both were $2.5T)  +

+

Fast forward to today, and stocks are worth 10x the total value of gold.

+

+

+

His point is that thinking of Bitcoin as “digital gold” is underselling how big it can be. Perhaps it will be on par with the S&P 500 (which today is a way that people store their wealth). 

+

+ 12:49 - Bitcoin is $43k today, where does it go?  +

+

“Bitcoin is the most honest market in the world. It went from $5k to $50K in the last 2 years, showing us that inflation was real, and the central banks are bankrupt. This is the end of the fiat money regime.” 

+

“Mr. Powell should be extremely grateful to Bitcoin. It’s the last warning they are going to get. They have chosen to ignore it, and will pay the consequences for it in the years ahead”

+

+ 14:00 - Who are the enemies of Bitcoin? Who is holding it back from a 10x or 100x rise? +

+

+ Bitcoin has 3 enemy types:  +

+

+ Enemy #1 - The Sociopathic Grandpa from Omaha  +

+

+

+

He’s honest about his hate for Bitcoin. He’s incentivized to hate Bitcoin for 2 reasons: 

+
+
    +
  1. He’s a winner of the current system 
  2. +
  3. He’s a money manager. If all people need to do is “buy bitcoin and chill” for a decade, all money managers are out of business
  4. +
+
+

+ Enemy #2 - The NYC Bankers  +

+

Like Jamie Dimon from JP Morgan. 

+

For years they called crypto “worthless.” Now it is undeniable, and they are losing customers, so they’re pivoting to offering crypto assets. 

+

+

+

+ Enemy #3 - Nameless, Faceless Bureaucrats +

+

These people hide behind a more passive-aggressive tone. They say things like “I see huge opportunity in blockchain” 

+

Anyone who says they are “pro-blockchain” is actually “anti-bitcoin” according to Peter. 

+

+ 20:36- The Grand Finale +

+

+

+

He ended with this hilarious slide design. He calls these enemies the “gerontocracy” (aka old rich). I gotta say, whoever photoshopped this slide is my hero. Pure art. Genius. Like Picasso in his prime. 

+

He says it’s OLD vs. YOUTH. And calls bitcoin a revolutionary youth movement. His final line: “We have to leave this conference and take over the world.”   +

+

+ *adds take-over-the-world to my to-do list* +

+

+ The Milk Road’s Take:  +

+

I’ve listened to Peter Thiel for over a decade. The guy is super smart and loves to go against the grain. He’s great at doing this, painting an enemy, and getting people’s attention with bold claims. 

+

On one hand, I love the conviction he has for Bitcoin, and think that it’s cool he’s been predicting this since 1999. 

+

On the other hand, I think he’s wrong about Ethereum, and I find the whole tribal / “us against the world” thing from the conference to be a turnoff. The macho attitude just feels very forced when nerds do it. 

+

I mean look at this. Michael Saylor came out for his talk and hit the crowd with the 1999 “Raise the Roof” move 🤣

+

+

+

+ That wasn’t all for Day 2… +

+

+ Jack Mallers, the founder of Strike, announced they have partnered with the POS  companies that power huge stores like Starbucks, McDonalds, Walmart & Walgreens to allow anyone to pay in-store with Bitcoin over the lightning network. Every credit card swipe has a 3% fee and is slow to settle. Lightning is essentially free (~0% fee) and settles instantly.  +

+

+ Cash App announces their new crypto services that will 1) let users auto-invest a percent of their paychecks into bitcoin and 2) round payments up to the nearest dollar to buy bitcoin with the difference (kinda like Acorns).  +

+

+ Robinhood announced they’re rolling out their new crypto wallet to 2M users. +

+

+ Ricardo Salinas, a Mexican billionaire, says his liquid portfolio is 60% Bitcoin. In 2020, Bitcoin only made up about 10% of his portfolio.  That’s what we call growth, baby! +

+

+

+

+

+ TODAY'S MILK ROAD IS BROUGHT TO YOU BY CRYPTOTRADER.TAX +

+

+

When it comes to taxes, there are two types of people. 

+
+
    +
  1. Those who do taxes early (also known as psychopaths)
  2. +
  3. The rest of us who wait until the last minute
  4. +
+
+

With less than 2 weeks left to file taxes, we’ve partnered with Cryptotrader.tax (soon to be CoinLedger) to make sure you got everything you need.

+

+ A common misconception: +

+

“I don’t get taxed until I cash out to fiat”. 

+

We hate to be the bearer of bad news, but you do in fact realize taxable events and capital gains when trading crypto > crypto.

+

It sucks, but that's the law for ya.

+

+

+

+

+ FUNDING FRIDAY +

+

+

+ Over $700M was raised in Web3 companies and funds this week! Who got the bread this time? +

+

+ Dank Bank got $4.2M to let people buy & sell Memes as NFTs. Sounds silly, but…Meme’s are a part of culture. They are globally recognizable. Maybe memes will be collected as other works of culture & art?  +

+

+ Fractal got $35M to build a new NFT gaming marketplace. This is Justin Kan’s new startup (previously JustinTV, Twitch). Worth keeping an eye on +

+

+ Leap got $10.2M to build out a “super” wallet for the Terra ecosystem that can show off your NFTs, track the value of your wallet, and integrate with the most popular Terra protocols +

+

+ GOALS got $15M to build the “FIFA of Web3” utilizing NFTs. The cool thing here is you’ll own all the assets in the game - from the players to the cleats they wear - and it can all be sold or traded. They still need to build an awesome game though.. +

+

+ Hivemapper got $18M to build a decentralized Google Maps by giving drivers digital tokens to put dashcams in their cars. You know we love easy ways to make money. (They’re leading the wave of IRL → tokens that we talked about here). +

+

Dive into our full database of companies that have fundraised here!

+

+

+

+

+ SIT BACK AND RELAX THIS WEEKEND WITH THE VITALIK SPECIAL +

+

+
+

Send a video of you drinking a Vitalik cocktail to @milkroaddaily on Twitter and we’ll retweet it!

+

+

+

+

+

See ya Monday!

+

+ Reviews from the Road: +

+

+ Warm... +

+

+

+

+ Getting warmer... +

+

+

+

+ Ahhh there it is... +

+

+

+

+ What'd you think of today's email? +

+ +

+

+
+
+
+
\ No newline at end of file diff --git a/packages/readabilityjs/test/test-pages/milkroad/source.html b/packages/readabilityjs/test/test-pages/milkroad/source.html new file mode 100644 index 000000000..c876b0391 --- /dev/null +++ b/packages/readabilityjs/test/test-pages/milkroad/source.html @@ -0,0 +1,958 @@ + + + + + + 🥛 Peter Thiel Calls Warren Buffett a Sociopathic Grandpa from Omaha + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+
+
+
+

+ 🥛 Peter Thiel Calls Warren Buffett a Sociopathic Grandpa from Omaha +

+
+

+ By Shaan Puri & Ben Levy +

+
+
+ +
+
+
+
+ +
+
+

+ GM. This is the Milk Road, we cut the crypto sandwich into triangles, just the way you like it.  +

+
+
+

+ Today's estimated read time: 4 minutes & 7 seconds +

+
+
+

+ We’re skipping price action today, because we have a LOT of juicy stuff to go over.  +

+
+
+

+ Juicy? What happened? +

+
+
+

+ Day 2 of The Bitcoin Conference was fire, so I watched all the talks and summarized all the important stuff for you. +

+
+
+ +
+
+

+ "WARREN BUFFETT IS A SOCIOPATHIC GRANDPA FROM OMAHA" - PETER THIEL +

+
+
+

+ What happened:  +

+
+
+

+ Peter Thiel (co–founder of Paypal, and first investor in Facebook) got on stage and gave a crazy talk.  +

+
+
+

+ I kept a running diary of my thoughts during his talk +

+
+
+

+ 0:00 - Wow, they got Peter Thiel to talk. *gets pen & notepad out* +

+
+
+

+ 0:04 - They start by playing a video clip of Peter talking at a conference in 1999. He’s making 2 predictions:  +

+
+
+
    +
  1. It’s 1999, but he’s predicting that 1B people will have cell phones connected to the internet in 5 years (he’s right, but it took about 10 years instead of 5) +
  2. +
  3. People will have money on their cell phone. It will be a currency that isn’t controlled by their local government (sounds like crypto, without saying crypto)  +
  4. +
+
+
+

+ 2:40 - OK, the real Peter is on stage now. Crowd cheers. They sound a little tipsy even though it’s 11am.. Nice. This is why we do conferences in Miami.  +

+
+
+

+ He starts off with a bold move. He’s waving a stack of $100 bills in the air. +

+
+
+

+ He asks: “what is money?,” “this always gets people's attention,” “it’s not good as toilet paper. It’s not wallpaper. But people want it. Do you want it? Come get it!” +

+
+
+ +
+
+

+ 3:16 - He throws the wad of cash at someone in the crowd then laughs as they fight over it: “I thought you guys were supposed to be Bitcon maximalists.” The joke lands. +

+
+
+

+ 3:30 - He talks about the early days of PayPal. Shows a picture of him and Elon Musk (they were both CEOs of Paypal early on). Pretty wild to think about how much talent was in 1 startup.  +

+
+
+

+ Here’s who worked at Paypal:  +

+
+
+
    +
  • Peter - created PayPal, first investor in Facebook +
  • +
  • Elon - CEO of Tesla, SpaceX +
  • +
  • The founders of YouTube +
  • +
  • The founder of LinkedIn +
  • +
  • The founder of Yelp  +
  • +
  • The founder of Kiva.org  +
  • +
  • The founder of Yammer +
  • +
  • The COO of Square +
  • +
  • The future CEO of Reddit +
  • +
+
+
+

+ This is called the PayPal mafia^. What a killer roster of talent. OK, back to Peter’s talk:  +

+
+
+

+ 3:55 - He shows a slide from the first pitch deck of PayPal. The initial idea was a lot like Bitcoin. A currency that lived outside of the banking system. He said “we didn’t know anything” back then, but they had big ambitions. +

+
+
+ +
+
+

+ 4:12 - But that was really hard. So within a couple years, they had created PayPal. Which worked with the banking system instead of replacing it. This was more practical, but less ambitious. It was just a payment system, not a new kind of money.  +

+
+
+

+ 6:49 - Payment system vs. Money. Why does it matter?  +

+
+
+

+ Peter points out an important thing about money. Velocity.  +

+
+
+

+ The simple idea is that people will spend their “less valuable” money first. Back in the day, people would hoard gold, and spend silver.  +

+
+
+

+ Today, people will save Bitcoin, and spend dollars. +

+
+
+

+ This means some money moves “fast” throughout the economy (hopping from person to person). To most people, that makes a currency more valuable! It’s being used! Must be more valuable, right? +

+
+
+

+ The velocity theory says that’s wrong. The slower moving money will end up being higher priced. Nobody is ‘holding’ the fast money, so it doesn’t go up in price (sellers willing to sell).  +

+
+
+

+ For a deep dive on this, read this Velocity of Money post. +

+
+
+

+ Anyways, Peter’s belief is that Bitcoin is “slow money” and therefore, it’s valuable money. He says Ethereum is a payments network, high velocity, and lower value. +

+
+
+ +
+
+

+ If Peter’s right, Bitcoin will end up a lot more valuable than ETH over time. (Editor’s note: I totally disagree. I think he’s wrong in thinking ETH is a payments network) +

+
+
+

+ 7:36 - OK, this is where the talk starts getting aggressive.  +

+
+
+

+ He puts up this slide. The crowd goes wild. Crypto is so tribal. +

+
+
+

+ He is trying to talk but you can barely hear him, because the red-blooded-american crowd is going wild for no reason. +

+
+
+ +
+
+

+ 8:13 - He says Bitcoin is like gold (a store of value). And ETH is like Visa (a payments network).  +

+
+
+

+ He points out that Gold has a total market value of $12T, and the biggest payment network (Visa) is worth 26x less.  +

+
+
+

+ If Peter is right, Bitcoin has more upside than ETH (again, I disagree,but let’s let Peter talk. He’s the billionaire chess grandmaster. I’m just a hairy dude with a free newsletter) +

+
+
+

+ 11:26 - He puts up this slide showing that back in the day (1980) Gold was equal to the total value of all stocks. (Both were $2.5T)  +

+
+
+

+ Fast forward to today, and stocks are worth 10x the total value of gold. +

+
+
+ +
+
+

+ His point is that thinking of Bitcoin as “digital gold” is underselling how big it can be. Perhaps it will be on par with the S&P 500 (which today is a way that people store their wealth).  +

+
+
+

+ 12:49 - Bitcoin is $43k today, where does it go?  +

+
+
+

+ “Bitcoin is the most honest market in the world. It went from $5k to $50K in the last 2 years, showing us that inflation was real, and the central banks are bankrupt. This is the end of the fiat money regime.”  +

+
+
+

+ “Mr. Powell should be extremely grateful to Bitcoin. It’s the last warning they are going to get. They have chosen to ignore it, and will pay the consequences for it in the years ahead” +

+
+
+

+ 14:00 - Who are the enemies of Bitcoin? Who is holding it back from a 10x or 100x rise? +

+
+
+

+ Bitcoin has 3 enemy types:  +

+
+
+

+ Enemy #1 - The Sociopathic Grandpa from Omaha  +

+
+
+ +
+
+

+ He’s honest about his hate for Bitcoin. He’s incentivized to hate Bitcoin for 2 reasons:  +

+
+
+
    +
  1. He’s a winner of the current system  +
  2. +
  3. He’s a money manager. If all people need to do is “buy bitcoin and chill” for a decade, all money managers are out of business +
  4. +
+
+
+

+ Enemy #2 - The NYC Bankers  +

+
+
+

+ Like Jamie Dimon from JP Morgan.  +

+
+
+

+ For years they called crypto “worthless.” Now it is undeniable, and they are losing customers, so they’re pivoting to offering crypto assets.  +

+
+
+

+ Now you go to GoldmanSachs.com and see stuff like this: +

+
+
+ +
+
+

+ Enemy #3 - Nameless, Faceless Bureaucrats +

+
+
+

+ These people hide behind a more passive-aggressive tone. They say things like “I see huge opportunity in blockchain”  +

+
+
+

+ Anyone who says they are “pro-blockchain” is actually “anti-bitcoin” according to Peter.  +

+
+
+

+ 20:36- The Grand Finale +

+
+
+ +
+
+

+ He ended with this hilarious slide design. He calls these enemies the “gerontocracy” (aka old rich). I gotta say, whoever photoshopped this slide is my hero. Pure art. Genius. Like Picasso in his prime.  +

+
+
+

+ He says it’s OLD vs. YOUTH. And calls bitcoin a revolutionary youth movement. His final line: “We have to leave this conference and take over the world.”   +

+
+
+

+ *adds take-over-the-world to my to-do list* +

+
+
+

+ The Milk Road’s Take:  +

+
+
+

+ I’ve listened to Peter Thiel for over a decade. The guy is super smart and loves to go against the grain. He’s great at doing this, painting an enemy, and getting people’s attention with bold claims.  +

+
+
+

+ On one hand, I love the conviction he has for Bitcoin, and think that it’s cool he’s been predicting this since 1999.  +

+
+
+

+ On the other hand, I think he’s wrong about Ethereum, and I find the whole tribal / “us against the world” thing from the conference to be a turnoff. The macho attitude just feels very forced when nerds do it.  +

+
+
+

+ I mean look at this. Michael Saylor came out for his talk and hit the crowd with the 1999 “Raise the Roof” move 🤣 +

+
+
+ +
+
+

+ That wasn’t all for Day 2… +

+
+
+

+ Jack Mallers, the founder of Strike, announced they have partnered with the POS  companies that power huge stores like Starbucks, McDonalds, Walmart & Walgreens to allow anyone to pay in-store with Bitcoin over the lightning network. Every credit card swipe has a 3% fee and is slow to settle. Lightning is essentially free (~0% fee) and settles instantly.  +

+
+
+

+ Cash App announces their new crypto services that will 1) let users auto-invest a percent of their paychecks into bitcoin and 2) round payments up to the nearest dollar to buy bitcoin with the difference (kinda like Acorns).  +

+
+
+

+ Robinhood announced they’re rolling out their new crypto wallet to 2M users. +

+
+
+

+ Ricardo Salinas, a Mexican billionaire, says his liquid portfolio is 60% Bitcoin. In 2020, Bitcoin only made up about 10% of his portfolio.  That’s what we call growth, baby! +

+
+
+ +
+
+

+ TODAY'S MILK ROAD IS BROUGHT TO YOU BY CRYPTOTRADER.TAX +

+
+
+

+ When it comes to taxes, there are two types of people.  +

+
+
+
    +
  1. Those who do taxes early (also known as psychopaths) +
  2. +
  3. The rest of us who wait until the last minute +
  4. +
+
+
+

+ With less than 2 weeks left to file taxes, we’ve partnered with Cryptotrader.tax (soon to be CoinLedger) to make sure you got everything you need. +

+
+
+

+ A common misconception: +

+
+
+

+ “I don’t get taxed until I cash out to fiat”.  +

+
+
+

+ We hate to be the bearer of bad news, but you do in fact realize taxable events and capital gains when trading crypto > crypto. +

+
+
+

+ It sucks, but that's the law for ya. +

+
+ +
+ +
+
+

+ FUNDING FRIDAY +

+
+
+

+ Over $700M was raised in Web3 companies and funds this week! Who got the bread this time? +

+
+
+

+ Dank Bank got $4.2M to let people buy & sell Memes as NFTs. Sounds silly, but…Meme’s are a part of culture. They are globally recognizable. Maybe memes will be collected as other works of culture & art?  +

+
+
+

+ Fractal got $35M to build a new NFT gaming marketplace. This is Justin Kan’s new startup (previously JustinTV, Twitch). Worth keeping an eye on +

+
+
+

+ Leap got $10.2M to build out a “super” wallet for the Terra ecosystem that can show off your NFTs, track the value of your wallet, and integrate with the most popular Terra protocols +

+
+
+

+ GOALS got $15M to build the “FIFA of Web3” utilizing NFTs. The cool thing here is you’ll own all the assets in the game - from the players to the cleats they wear - and it can all be sold or traded. They still need to build an awesome game though.. +

+
+
+

+ Hivemapper got $18M to build a decentralized Google Maps by giving drivers digital tokens to put dashcams in their cars. You know we love easy ways to make money. (They’re leading the wave of IRL → tokens that we talked about here). +

+
+
+

+ Dive into our full database of companies that have fundraised here! +

+
+
+ +
+
+

+ SIT BACK AND RELAX THIS WEEKEND WITH THE VITALIK SPECIAL +

+
+
+ +
+
+

+ Send a video of you drinking a Vitalik cocktail to @milkroaddaily on Twitter and we’ll retweet it! +

+
+
+ +
+
+ +
+
+

+ See ya Monday! +

+
+ +
+

+ Reviews from the Road: +

+
+
+

+ Warm... +

+
+
+ +
+
+

+ Getting warmer... +

+
+
+ +
+
+

+ Ahhh there it is... +

+
+
+ +
+
+

+ What'd you think of today's email? +

+
+ +
+ +
+
+
+
+
+
+
+ +
+
+

+ Subscribe to Milk Road +

+

+ The newsletter that makes you smarter about web3 +

+
+ +
+ + +
+ +
+
+ +
+
+
+

+ This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply. +

+
+
+
+
+ +
+
+ +
+ +
+
+ +
+ +
+ + diff --git a/packages/readabilityjs/test/test-pages/milkroad/url.txt b/packages/readabilityjs/test/test-pages/milkroad/url.txt new file mode 100644 index 000000000..f81e013db --- /dev/null +++ b/packages/readabilityjs/test/test-pages/milkroad/url.txt @@ -0,0 +1 @@ +https://www.milkroad.com/p/peter-thiel-calls-warren-buffett-sociopathic-grandpa-omaha \ No newline at end of file