diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/RecommendationGroupView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/RecommendationGroupView.swift
index 30356f083..3367c28ca 100644
--- a/apple/OmnivoreKit/Sources/App/Views/Profile/RecommendationGroupView.swift
+++ b/apple/OmnivoreKit/Sources/App/Views/Profile/RecommendationGroupView.swift
@@ -5,7 +5,7 @@ import Views
@MainActor final class RecommendationsGroupViewModel: ObservableObject {
@Published var isLoading = false
- @Published var networkError = true
+ @Published var networkError = false
@Published var recommendationGroup: InternalRecommendationGroup
init(recommendationGroup: InternalRecommendationGroup) {
@@ -71,8 +71,6 @@ struct RecommendationGroupView: View {
@EnvironmentObject var dataService: DataService
@StateObject var viewModel: RecommendationsGroupViewModel
- @State var presentShareSheet = false
-
var body: some View {
Group {
#if os(iOS)
@@ -110,12 +108,22 @@ struct RecommendationGroupView: View {
private var membersSection: some View {
Section("Members") {
- ForEach(viewModel.nonAdmins) { member in
- SmallUserCard(data: ProfileCardData(
- name: member.name,
- username: member.username,
- imageURL: member.profileImageURL != nil ? URL(string: member.profileImageURL!) : nil
- ))
+ if viewModel.nonAdmins.count > 0 {
+ ForEach(viewModel.nonAdmins) { member in
+ SmallUserCard(data: ProfileCardData(
+ name: member.name,
+ username: member.username,
+ imageURL: member.profileImageURL != nil ? URL(string: member.profileImageURL!) : nil
+ ))
+ }
+ } else {
+ Text("""
+ This group does not have any members. Add users to your group by sending
+ them the invite link.
+
+ [Learn more about groups](https://blog.omnivore.app/p/dca38ba4-8a74-42cc-90ca-d5ffa5d075cc)
+ """)
+ .accentColor(.blue)
}
}
}
@@ -128,7 +136,17 @@ struct RecommendationGroupView: View {
Section("Invite Link") {
Button(action: {
- presentShareSheet = true
+ #if os(iOS)
+ UIPasteboard.general.string = viewModel.recommendationGroup.inviteUrl
+ #endif
+
+ #if os(macOS)
+ let pasteBoard = NSPasteboard.general
+ pasteBoard.clearContents()
+ pasteBoard.writeObjects([highlightParams.quote as NSString])
+ #endif
+
+ Snackbar.show(message: "Invite link copied")
}, label: {
Text("[\(viewModel.recommendationGroup.inviteUrl)](\(viewModel.recommendationGroup.inviteUrl))")
.font(.appCaption)
@@ -138,9 +156,6 @@ struct RecommendationGroupView: View {
adminsSection
membersSection
}
- .formSheet(isPresented: $presentShareSheet) {
- shareView
- }
.navigationTitle(viewModel.recommendationGroup.name)
}
}
diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/RecommendationGroupsView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/RecommendationGroupsView.swift
index 570072b41..f2896030a 100644
--- a/apple/OmnivoreKit/Sources/App/Views/Profile/RecommendationGroupsView.swift
+++ b/apple/OmnivoreKit/Sources/App/Views/Profile/RecommendationGroupsView.swift
@@ -6,7 +6,7 @@ import Views
@MainActor final class RecommendationsGroupsViewModel: ObservableObject {
@Published var isLoading = false
@Published var isCreating = false
- @Published var networkError = true
+ @Published var networkError = false
@Published var recommendationGroups = [InternalRecommendationGroup]()
@Published var showCreateSheet = false
@@ -130,14 +130,29 @@ struct GroupsView: View {
.disabled(viewModel.isLoading)
}
- Section(header: Text("Your recommendation groups")) {
- ForEach(viewModel.recommendationGroups) { recommendationGroup in
- NavigationLink(
- destination: RecommendationGroupView(viewModel: RecommendationsGroupViewModel(recommendationGroup: recommendationGroup))
- ) {
- Text(recommendationGroup.name)
+ if viewModel.recommendationGroups.count > 0 {
+ Section(header: Text("Your recommendation groups")) {
+ ForEach(viewModel.recommendationGroups) { recommendationGroup in
+ NavigationLink(
+ destination: RecommendationGroupView(viewModel: RecommendationsGroupViewModel(recommendationGroup: recommendationGroup))
+ ) {
+ Text(recommendationGroup.name)
+ }
}
}
+ } else {
+ Section {
+ Text("""
+ You are not a member of any groups.
+ Create a new group and send the invite link to your friends get started.
+
+ During the beta you are limited to creating three groups, and each group
+ can have a maximum of twelve users.
+
+ [Learn more about groups](https://blog.omnivore.app/p/dca38ba4-8a74-42cc-90ca-d5ffa5d075cc)
+ """)
+ .accentColor(.blue)
+ }
}
}
.navigationTitle("Recommendation Groups")
diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift
index 26532feda..fcf3d0f37 100644
--- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift
+++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift
@@ -47,30 +47,35 @@ struct InnerRootView: View {
@ViewBuilder private var innerBody: some View {
if authenticator.isLoggedIn {
- PrimaryContentView()
- .onAppear {
- viewModel.triggerPushNotificationRequestIfNeeded()
- }
- #if os(iOS)
- .miniPlayer()
- #endif
- .snackBar(isShowing: $viewModel.showSnackbar, message: viewModel.snackbarMessage)
- // Schedule the dismissal every time we present the snackbar.
- .onChange(of: viewModel.showSnackbar) { newValue in
- if newValue {
- DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
- withAnimation {
- viewModel.showSnackbar = false
+ GeometryReader { geo in
+ PrimaryContentView()
+ .onAppear {
+ viewModel.triggerPushNotificationRequestIfNeeded()
+ }
+ #if os(iOS)
+ .miniPlayer()
+ .formSheet(isPresented: $viewModel.showNewFeaturePrimer, modalSize: CGSize(width: geo.size.width * 0.66, height: geo.size.width * 0.66)) {
+ FeaturePrimer.recommendationsPrimer
+ }
+ .onAppear {
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) {
+ viewModel.showNewFeaturePrimer = viewModel.shouldShowNewFeaturePrimer
+ viewModel.shouldShowNewFeaturePrimer = false
+ }
+ }
+ #endif
+ .snackBar(isShowing: $viewModel.showSnackbar, message: viewModel.snackbarMessage)
+ // Schedule the dismissal every time we present the snackbar.
+ .onChange(of: viewModel.showSnackbar) { newValue in
+ if newValue {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
+ withAnimation {
+ viewModel.showSnackbar = false
+ }
}
}
}
- }
- #if os(iOS)
- .customAlert(isPresented: $viewModel.showPushNotificationPrimer) {
- pushNotificationPrimerView
- }
- #endif
-
+ }
} else {
WelcomeView()
.accessibilityElement()
@@ -105,14 +110,14 @@ struct InnerRootView: View {
}
#if os(iOS)
- private var pushNotificationPrimerView: PushNotificationPrimer {
- PushNotificationPrimer(
- acceptAction: { viewModel.handlePushNotificationPrimerAcceptance() },
- denyAction: {
- UserDefaults.standard.set(true, forKey: UserDefaultKey.userHasDeniedPushPrimer.rawValue)
- viewModel.showPushNotificationPrimer = false
- }
- )
- }
+// private var pushNotificationPrimerView: PushNotificationPrimer {
+// PushNotificationPrimer(
+// acceptAction: { viewModel.handlePushNotificationPrimerAcceptance() },
+// denyAction: {
+// UserDefaults.standard.set(true, forKey: UserDefaultKey.userHasDeniedPushPrimer.rawValue)
+// viewModel.showPushNotificationPrimer = false
+// }
+// )
+// }
#endif
}
diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift
index 92eec8ce0..200093972 100644
--- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift
+++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift
@@ -14,7 +14,9 @@ import Views
public final class RootViewModel: ObservableObject {
let services = Services()
- @Published public var showPushNotificationPrimer = false
+ @Published public var showNewFeaturePrimer = false
+ @AppStorage(UserDefaultKey.shouldShowNewFeaturePrimer.rawValue) var shouldShowNewFeaturePrimer = true
+
@Published var snackbarMessage: String?
@Published var showSnackbar = false
@Published var showMiniPlayer = true
@@ -34,32 +36,32 @@ public final class RootViewModel: ObservableObject {
}
func triggerPushNotificationRequestIfNeeded() {
- guard FeatureFlag.enablePushNotifications else { return }
-
- if UserDefaults.standard.bool(forKey: UserDefaultKey.userHasDeniedPushPrimer.rawValue) {
- return
- }
-
- #if os(iOS)
- UNUserNotificationCenter.current().getNotificationSettings { [weak self] settings in
- switch settings.authorizationStatus {
- case .notDetermined:
- DispatchQueue.main.async {
- self?.showPushNotificationPrimer = true
- }
- case .authorized, .provisional, .ephemeral, .denied:
- return
- @unknown default:
- return
- }
- }
- #endif
+// guard FeatureFlag.enablePushNotifications else { return }
+//
+// if UserDefaults.standard.bool(forKey: UserDefaultKey.userHasDeniedPushPrimer.rawValue) {
+// return
+// }
+//
+// #if os(iOS)
+// UNUserNotificationCenter.current().getNotificationSettings { [weak self] settings in
+// switch settings.authorizationStatus {
+// case .notDetermined:
+// DispatchQueue.main.async {
+// self?.showPushNotificationPrimer = true
+// }
+// case .authorized, .provisional, .ephemeral, .denied:
+// return
+// @unknown default:
+// return
+// }
+// }
+// #endif
}
#if os(iOS)
func handlePushNotificationPrimerAcceptance() {
- showPushNotificationPrimer = false
- UNUserNotificationCenter.current().requestAuth()
+// showPushNotificationPrimer = false
+// UNUserNotificationCenter.current().requestAuth()
}
#endif
}
diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/RecommendToView.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/RecommendToView.swift
index 20d737d03..acb9ad83b 100644
--- a/apple/OmnivoreKit/Sources/App/Views/WebReader/RecommendToView.swift
+++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/RecommendToView.swift
@@ -5,12 +5,13 @@ import Views
@MainActor final class RecommendToViewModel: ObservableObject {
@Published var isLoading = false
- @Published var networkError = true
+ @Published var networkError = false
@Published var recommendationGroups = [InternalRecommendationGroup]()
- @Published var selectedGroups = [String]()
+ @Published var selectedGroups = [InternalRecommendationGroup]()
@Published var isRunning = false
@Published var showError = false
- @Published var note: String?
+ @Published var showNoteView = false
+ @Published var note: String = ""
let pageID: String
@@ -35,7 +36,7 @@ import Views
isRunning = true
do {
- try await dataService.recommendPage(pageID: pageID, groupIDs: selectedGroups, note: note)
+ try await dataService.recommendPage(pageID: pageID, groupIDs: selectedGroups.map(\.id), note: note.isEmpty ? nil : note)
} catch {
showError = true
}
@@ -50,6 +51,16 @@ struct RecommendToView: View {
@Environment(\.dismiss) private var dismiss
var nextButton: some View {
+ Button(action: {
+ self.viewModel.showNoteView = true
+ }, label: {
+ Text("Next")
+ .bold()
+ })
+ .disabled(viewModel.selectedGroups.isEmpty)
+ }
+
+ var sendButton: some View {
if viewModel.isRunning {
return AnyView(ProgressView())
} else {
@@ -60,15 +71,60 @@ struct RecommendToView: View {
dismiss()
}
}, label: {
- Text("Next")
+ Text("Send")
+ .bold()
})
.disabled(viewModel.selectedGroups.isEmpty)
)
}
}
+ var noteView: some View {
+ VStack {
+ HStack {
+ Text("To:")
+ .font(.appCaption)
+ .foregroundColor(.appGrayText)
+ Text(InternalRecommendationGroup.readable(list: viewModel.selectedGroups))
+ .font(.appCaption)
+ .foregroundColor(.appGrayTextContrast)
+ Spacer()
+ }
+ TextEditor(text: $viewModel.note)
+ .lineSpacing(6)
+ .accentColor(.appGraySolid)
+ .foregroundColor(.appGrayTextContrast)
+ .font(.appBody)
+ .padding(12)
+ .frame(height: 200)
+ .background(
+ RoundedRectangle(cornerRadius: 8)
+ .strokeBorder(Color.appGrayBorder, lineWidth: 1)
+ .background(RoundedRectangle(cornerRadius: 8).fill(Color.systemBackground))
+ )
+ .overlay(
+ Text("Add a note (optional)")
+ .allowsHitTesting(false)
+ .opacity(viewModel.note.isEmpty ? 0.4 : 0.0)
+ .font(.appBody)
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
+ .padding(.top, 24)
+ .padding(.leading, 16)
+ )
+ Spacer()
+ }
+ .padding(16)
+ .navigationBarTitleDisplayMode(.inline)
+ .navigationViewStyle(.stack)
+ .navigationBarItems(trailing: sendButton)
+ }
+
var body: some View {
VStack {
+ NavigationLink(destination: noteView,
+ isActive: $viewModel.showNoteView) {
+ EmptyView()
+ }
List {
Section("Select groups to recommend to") {
ForEach(viewModel.recommendationGroups) { group in
@@ -77,17 +133,17 @@ struct RecommendToView: View {
Spacer()
- if viewModel.selectedGroups.contains(group.id) {
+ if viewModel.selectedGroups.contains(where: { $0.id == group.id }) {
Image(systemName: "checkmark")
}
}
.contentShape(Rectangle())
.onTapGesture {
- let idx = viewModel.selectedGroups.firstIndex(of: group.id)
+ let idx = viewModel.selectedGroups.firstIndex(where: { $0.id == group.id })
if let idx = idx {
viewModel.selectedGroups.remove(at: idx)
} else {
- viewModel.selectedGroups.append(group.id)
+ viewModel.selectedGroups.append(group)
}
}
}
@@ -105,7 +161,6 @@ struct RecommendToView: View {
}
)
}
- .navigationBarTitle("Recommend")
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(.stack)
.navigationBarItems(leading: Button(action: {
diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContent.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContent.swift
index 6aaa82bcf..ab2cea59d 100644
--- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContent.swift
+++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContent.swift
@@ -78,6 +78,7 @@ struct WebReaderContent {
readingProgressAnchorIndex: \(item.readingProgressAnchor),
labels: \(item.labelsJSONString),
highlights: \(articleContent.highlightsJSONString),
+ recommendations: \(item.recommendationsJSONString),
}
window.fontSize = \(textFontSize)
diff --git a/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents b/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents
index deb3afd4f..4ca8869bd 100644
--- a/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents
+++ b/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents
@@ -55,7 +55,7 @@
-
+
@@ -95,8 +95,9 @@
+
-
+
diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift
index 0459d5b59..529d3204e 100644
--- a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift
+++ b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift
@@ -129,6 +129,26 @@ public extension LinkedItem {
return String(data: JSON, encoding: .utf8) ?? "[]"
}
+ var recommendationsJSONString: String {
+ let recommendations = self.recommendations.asArray(of: Recommendation.self).map { recommendation in
+ let recommendedAt = recommendation.recommendedAt == nil ? nil : recommendation.recommendedAt?.ISO8601Format()
+ return [
+ "id": NSString(string: recommendation.id ?? ""),
+ "name": NSString(string: recommendation.name ?? ""),
+ "note": recommendation.note == nil ? nil : NSString(string: recommendation.note ?? ""),
+ "user": recommendation.user == nil ? nil : NSDictionary(dictionary: [
+ "userID": NSString(string: recommendation.user?.userID ?? ""),
+ "name": NSString(string: recommendation.user?.name ?? ""),
+ "username": NSString(string: recommendation.user?.username ?? ""),
+ "profileImageURL": recommendation.user?.profileImageURL == nil ? nil : NSString(string: recommendation.user?.profileImageURL ?? "")
+ ]),
+ "recommendedAt": recommendedAt == nil ? nil : NSString(string: recommendedAt!)
+ ]
+ }
+ guard let JSON = (try? JSONSerialization.data(withJSONObject: recommendations, options: .prettyPrinted)) else { return "[]" }
+ return String(data: JSON, encoding: .utf8) ?? "[]"
+ }
+
var formattedByline: String {
var byline = ""
diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/Recommendation.swift b/apple/OmnivoreKit/Sources/Models/DataModels/Recommendation.swift
index 279a74268..dc2006c33 100644
--- a/apple/OmnivoreKit/Sources/Models/DataModels/Recommendation.swift
+++ b/apple/OmnivoreKit/Sources/Models/DataModels/Recommendation.swift
@@ -18,4 +18,30 @@ public extension Recommendation {
return recommendation
}
+
+ static func byline(_ set: NSSet) -> String {
+ Array(set).reduce("") { str, item in
+ if let recommendation = item as? Recommendation, let userName = recommendation.user?.name {
+ if str.isEmpty {
+ return userName
+ } else {
+ return str + ", " + userName
+ }
+ }
+ return str
+ }
+ }
+
+ static func groupsLine(_ set: NSSet) -> String {
+ Array(set).reduce("") { str, item in
+ if let recommendation = item as? Recommendation, let name = recommendation.name {
+ if str.isEmpty {
+ return name
+ } else {
+ return str + ", " + name
+ }
+ }
+ return str
+ }
+ }
}
diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift
index 51ded19f7..8e7be6b56 100644
--- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift
+++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift
@@ -225,37 +225,22 @@ extension DataService {
}
let recommendingUserSelection = Selection.RecommendingUser {
- do {
- return InternalUserProfile(
- userID: try $0.userId(),
- name: try $0.name(),
- username: try $0.username(),
- profileImageURL: nil // try $0.profileImageUrl() ?? nil
- )
- } catch {
- print("ERROR WITH recommendingUserSelection", error)
- throw error
- }
+ InternalUserProfile(
+ userID: try $0.userId(),
+ name: try $0.name(),
+ username: try $0.username(),
+ profileImageURL: nil // try $0.profileImageUrl() ?? nil
+ )
}
let recommendationSelection = Selection.Recommendation {
- do {
- let result = InternalRecommendation(
- id: try $0.id(),
- name: try $0.name(),
- user: try $0.user(selection: recommendingUserSelection.nullable),
- recommendedAt: try $0.recommendedAt().value ?? Date()
- )
- return result
- } catch {
- print("ERROR WITH recommendationSelection", error)
- throw error
- }
-}
-
-private func emptyrecommended() -> [InternalRecommendation] {
- print("got the empty InternalRecommendation")
- return []
+ InternalRecommendation(
+ id: try $0.id(),
+ name: try $0.name(),
+ note: try $0.note(),
+ user: try $0.user(selection: recommendingUserSelection.nullable),
+ recommendedAt: try $0.recommendedAt().value ?? Date()
+ )
}
private let libraryArticleSelection = Selection.Article {
@@ -283,7 +268,7 @@ private let libraryArticleSelection = Selection.Article {
contentReader: try $0.contentReader().rawValue,
originalHtml: nil,
language: try $0.language(),
- recommendations: try $0.recommendations(selection: recommendationSelection.list.nullable) ?? emptyrecommended(),
+ recommendations: try $0.recommendations(selection: recommendationSelection.list.nullable) ?? [],
labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? []
)
}
diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendation.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendation.swift
index 80a33b0cc..e861acf30 100644
--- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendation.swift
+++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendation.swift
@@ -5,6 +5,7 @@ import Models
public struct InternalRecommendation {
let id: String
let name: String
+ let note: String?
let user: InternalUserProfile?
let recommendedAt: Date
@@ -13,6 +14,7 @@ public struct InternalRecommendation {
let recommendation = existing ?? Recommendation(entity: Recommendation.entity(), insertInto: context)
recommendation.id = id
recommendation.name = name
+ recommendation.note = note
recommendation.recommendedAt = recommendedAt
recommendation.user = user?.asManagedObject(inContext: context)
return recommendation
@@ -29,6 +31,7 @@ public struct InternalRecommendation {
return InternalRecommendation(
id: id,
name: name,
+ note: recommendation.note,
user: InternalUserProfile.makeSingle(recommendation.user),
recommendedAt: recommendedAt
)
diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendationGroup.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendationGroup.swift
index ff90cc253..dbfdfdffd 100644
--- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendationGroup.swift
+++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendationGroup.swift
@@ -46,6 +46,16 @@ public struct InternalRecommendationGroup: Identifiable {
}
return nil
}
+
+ public static func readable(list: [InternalRecommendationGroup]) -> String {
+ list.reduce("") { str, group in
+ if str.isEmpty {
+ return group.name
+ } else {
+ return str + ", " + group.name
+ }
+ }
+ }
}
extension Sequence where Element == InternalRecommendationGroup {
diff --git a/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift b/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift
index ed93b0045..3e7e7eb1a 100644
--- a/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift
+++ b/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift
@@ -23,4 +23,5 @@ public enum UserDefaultKey: String {
case recentSearchTerms
case audioPlayerExpanded
case themeName
+ case shouldShowNewFeaturePrimer
}
diff --git a/apple/OmnivoreKit/Sources/Views/FeaturePrimer.swift b/apple/OmnivoreKit/Sources/Views/FeaturePrimer.swift
new file mode 100644
index 000000000..e67d35591
--- /dev/null
+++ b/apple/OmnivoreKit/Sources/Views/FeaturePrimer.swift
@@ -0,0 +1,69 @@
+//
+// File.swift
+//
+//
+// Created by Jackson Harper on 12/7/22.
+//
+
+import Foundation
+import SwiftUI
+
+public struct FeaturePrimer: View {
+ let isBeta: Bool
+ let title: String
+ let message: String
+ @Environment(\.dismiss) private var dismiss
+
+ public var body: some View {
+ VStack(spacing: 0) {
+ VStack(alignment: .leading) {
+ Text(title)
+ .font(.textToSpeechRead)
+ .foregroundColor(Color.appGrayTextContrast)
+ .frame(maxWidth: .infinity, alignment: .leading)
+
+ if isBeta {
+ HStack {
+ TextChip(text: "¡ Beta !", color: Color.red)
+ .frame(alignment: .leading)
+ TextChip(text: "¡ New Feature !", color: Color.green)
+ .frame(alignment: .leading)
+ }
+ }
+ }
+
+ ScrollView {
+ Text((try? AttributedString(markdown: message,
+ options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace))) ?? "")
+ .foregroundColor(Color.appGrayText)
+ .accentColor(.blue)
+ .padding(.bottom, 16)
+ }
+ .padding(.top, 16)
+
+ Spacer()
+
+ Button(action: {
+ dismiss()
+ }, label: { Text("Dismiss") })
+ .buttonStyle(PlainButtonStyle())
+ }.padding()
+ }
+
+ public static var recommendationsPrimer: some View {
+ FeaturePrimer(
+ isBeta: true,
+ title: "Introducing Recommendation Groups",
+ message: """
+ Recommendation groups make it easy to share great reads with friends and co-workers.
+
+ To get started, create a Recommendation Group from the profile page and invite some friends.
+
+ *During the beta you can create a max of three groups. Group sizes are limited to 12 people.*
+
+ [Learn more about groups](https://blog.omnivore.app/p/dca38ba4-8a74-42cc-90ca-d5ffa5d075cc)
+
+ """
+ )
+ }
+}
diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/HomeFeedCardView.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/HomeFeedCardView.swift
index 027bc4427..513842b49 100644
--- a/apple/OmnivoreKit/Sources/Views/FeedItem/HomeFeedCardView.swift
+++ b/apple/OmnivoreKit/Sources/Views/FeedItem/HomeFeedCardView.swift
@@ -88,19 +88,10 @@ public struct FeedCard: View {
}
if let recommendations = item.recommendations, recommendations.count > 0 {
- let byStr = recommendations.reduce("") { str, item in
- if let item = item as? Recommendation, let name = item.user?.name {
- return str + name
- }
- return str
- }
- let inStr = recommendations.reduce("") { str, item in
- if let item = item as? Recommendation, let name = item.name {
- return str + name
- }
- return str
- }
+ let byStr = Recommendation.byline(recommendations)
+ let inStr = Recommendation.groupsLine(recommendations)
HStack {
+ Image(systemName: "sparkles")
Text("Recommended by \(byStr) in \(inStr)")
.font(.appCaption)
.frame(alignment: .leading)
diff --git a/apple/OmnivoreKit/Sources/Views/FormSheetWrapper.swift b/apple/OmnivoreKit/Sources/Views/FormSheetWrapper.swift
index 4bab1d0d8..56d26bd3d 100644
--- a/apple/OmnivoreKit/Sources/Views/FormSheetWrapper.swift
+++ b/apple/OmnivoreKit/Sources/Views/FormSheetWrapper.swift
@@ -49,7 +49,7 @@ import SwiftUI
controller.view.sizeToFit()
controller.modalPresentationStyle = .formSheet
controller.modalTransitionStyle = .crossDissolve
- controller.preferredContentSize = CGSize(width: 320, height: 320)
+ controller.preferredContentSize = modalSize
}
controller.presentationController?.delegate = self
diff --git a/packages/web/components/elements/StyledText.tsx b/packages/web/components/elements/StyledText.tsx
index 1f7b3a4f3..618bb30df 100644
--- a/packages/web/components/elements/StyledText.tsx
+++ b/packages/web/components/elements/StyledText.tsx
@@ -22,6 +22,28 @@ const textVariants = {
fontSize: '$2',
lineHeight: '1.25',
},
+ recommendedByline: {
+ // fontWeight: 'bold',
+ fontSize: '13.5px',
+ paddingTop: '4px',
+ mt: '0px',
+ mb: '24px',
+ color: '$grayText',
+ },
+ userName: {
+ fontWeight: '600',
+ fontSize: '13.5px',
+ paddingTop: '4px',
+ my: '6px',
+ color: '$grayText',
+ },
+ userNote: {
+ fontSize: '16px',
+ paddingTop: '0px',
+ marginTop: '0px',
+ lineHeight: '1.5',
+ color: '$grayTextContrast',
+ },
headline: {
fontSize: '$4',
'@md': {
diff --git a/packages/web/components/templates/article/ArticleContainer.tsx b/packages/web/components/templates/article/ArticleContainer.tsx
index 4ee3a3285..aab1d0da1 100644
--- a/packages/web/components/templates/article/ArticleContainer.tsx
+++ b/packages/web/components/templates/article/ArticleContainer.tsx
@@ -1,12 +1,25 @@
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
import { Article } from './../../../components/templates/article/Article'
-import { Box, SpanBox, VStack } from './../../elements/LayoutPrimitives'
+import {
+ Blockquote,
+ Box,
+ HStack,
+ SpanBox,
+ VStack,
+} from './../../elements/LayoutPrimitives'
import { StyledText } from './../../elements/StyledText'
import { ArticleSubtitle } from './../../patterns/ArticleSubtitle'
-import { theme, ThemeId } from './../../tokens/stitches.config'
+import { styled, theme, ThemeId } from './../../tokens/stitches.config'
import { HighlightsLayer } from '../../templates/article/HighlightsLayer'
import { Button } from '../../elements/Button'
-import { MutableRefObject, useEffect, useState, useRef } from 'react'
+import {
+ MutableRefObject,
+ useEffect,
+ useState,
+ useRef,
+ useReducer,
+ useMemo,
+} from 'react'
import { ReportIssuesModal } from './ReportIssuesModal'
import { reportIssueMutation } from '../../../lib/networking/mutations/reportIssueMutation'
import { ArticleHeaderToolbar } from './ArticleHeaderToolbar'
@@ -19,6 +32,9 @@ import {
HighlightLocation,
makeHighlightStartEndOffset,
} from '../../../lib/highlights/highlightGenerator'
+import { Recommendation } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
+import { Avatar } from '../../elements/Avatar'
+import { Sparkle } from 'phosphor-react'
type ArticleContainerProps = {
article: ArticleAttributes
@@ -41,12 +57,15 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
const [showReportIssuesModal, setShowReportIssuesModal] = useState(false)
const [fontSize, setFontSize] = useState(props.fontSize ?? 20)
// iOS app embed can overide the original margin and line height
- const [maxWidthPercentageOverride, setMaxWidthPercentageOverride] =
- useState(null)
- const [lineHeightOverride, setLineHeightOverride] =
- useState(null)
- const [fontFamilyOverride, setFontFamilyOverride] =
- useState(null)
+ const [maxWidthPercentageOverride, setMaxWidthPercentageOverride] = useState<
+ number | null
+ >(null)
+ const [lineHeightOverride, setLineHeightOverride] = useState(
+ null
+ )
+ const [fontFamilyOverride, setFontFamilyOverride] = useState(
+ null
+ )
const [highContrastFont, setHighContrastFont] = useState(
props.highContrastFont ?? false
)
@@ -196,6 +215,31 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
readerHeadersColor: theme.colors.readerHeader.toString(),
}
+ const recommendationByline = useMemo(() => {
+ return props.article.recommendations
+ ?.flatMap((recommendation) => {
+ return recommendation.user?.name
+ })
+ .join(', ')
+ }, [props.article.recommendations])
+
+ const recommendationsWithNotes = useMemo(() => {
+ return (
+ props.article.recommendations?.filter((recommendation) => {
+ return recommendation.note
+ }) ?? []
+ )
+ }, [props.article.recommendations])
+
+ const StyledQuote = styled(Blockquote, {
+ margin: '0px 0px 0px 0px',
+ fontSize: '18px',
+ lineHeight: '27px',
+ color: '$grayText',
+ padding: '0px 16px',
+ borderLeft: '2px solid $omnivoreCtaYellow',
+ })
+
return (
<>
) : null}
+ {recommendationByline && (
+
+
+
+
+ Recommended by {recommendationByline}
+
+
+
+ {recommendationsWithNotes.map((item, idx) => (
+
+ {/* {item.note} */}
+
+
+
+ {item.user?.name}:
+ {' '}
+ {item.note}
+
+ :
+
+
+ ))}
+
+ )}