diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/RecommendToView.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/RecommendToView.swift
index 902eae6aa..24b624a91 100644
--- a/apple/OmnivoreKit/Sources/App/Views/WebReader/RecommendToView.swift
+++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/RecommendToView.swift
@@ -23,6 +23,7 @@ import Views
do {
recommendationGroups = try await dataService.recommendationGroups()
} catch {
+ print("ERROR fetching recommendationGroups: ", error)
networkError = true
}
@@ -68,23 +69,25 @@ struct RecommendToView: View {
var body: some View {
VStack {
List {
- ForEach(viewModel.recommendationGroups) { group in
- HStack {
- Text(group.name)
+ Section("Select groups to recommend to") {
+ ForEach(viewModel.recommendationGroups) { group in
+ HStack {
+ Text(group.name)
- Spacer()
+ Spacer()
- if viewModel.selectedGroups.contains(group.id) {
- Image(systemName: "checkmark")
+ if viewModel.selectedGroups.contains(group.id) {
+ Image(systemName: "checkmark")
+ }
}
- }
- .contentShape(Rectangle())
- .onTapGesture {
- let idx = viewModel.selectedGroups.firstIndex(of: group.id)
- if let idx = idx {
- viewModel.selectedGroups.remove(at: idx)
- } else {
- viewModel.selectedGroups.append(group.id)
+ .contentShape(Rectangle())
+ .onTapGesture {
+ let idx = viewModel.selectedGroups.firstIndex(of: group.id)
+ if let idx = idx {
+ viewModel.selectedGroups.remove(at: idx)
+ } else {
+ viewModel.selectedGroups.append(group.id)
+ }
}
}
}
@@ -101,7 +104,7 @@ struct RecommendToView: View {
}
)
}
- .navigationBarTitle("Recommend To")
+ .navigationBarTitle("Recommend")
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(.stack)
.navigationBarItems(leading: Button(action: {
diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift
index 9da188077..36111be75 100644
--- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift
+++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift
@@ -29,7 +29,7 @@ struct WebReaderContainerView: View {
@State private var bottomBarOpacity = 0.0
@State private var errorAlertMessage: String?
@State private var showErrorAlertMessage = false
- @State private var displayRecommendSheet = false
+ @State private var showRecommendSheet = false
@EnvironmentObject var dataService: DataService
@EnvironmentObject var audioController: AudioController
@@ -151,8 +151,8 @@ struct WebReaderContainerView: View {
}).frame(width: 48, height: 48)
Divider().opacity(0.8)
- Button(action: share, label: {
- Image(systemName: "square.and.arrow.up")
+ Button(action: recommend, label: {
+ Image(systemName: "sparkles")
}).frame(width: 48, height: 48)
// TODO: We don't have a single note function yet
@@ -223,7 +223,7 @@ struct WebReaderContainerView: View {
Button(
action: {
// dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0, anchorIndex: 0)
- displayRecommendSheet = true
+ showRecommendSheet = true
},
label: { Label("Recommend", systemImage: "sparkles") }
)
@@ -359,7 +359,7 @@ struct WebReaderContainerView: View {
showErrorAlertMessage = false
})
}
- .formSheet(isPresented: $displayRecommendSheet) {
+ .formSheet(isPresented: $showRecommendSheet) {
NavigationView {
RecommendToView(
dataService: dataService,
@@ -456,6 +456,10 @@ struct WebReaderContainerView: View {
Snackbar.show(message: !item.isArchived ? "Link archived" : "Link moved to Inbox")
}
+ func recommend() {
+ showRecommendSheet = true
+ }
+
func share() {
shareActionID = UUID()
}
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 4b026f83e..0e08359d7 100644
--- a/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents
+++ b/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents
@@ -55,6 +55,7 @@
+
@@ -91,6 +92,12 @@
+
+
+
+
+
+
diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/Recommendation.swift b/apple/OmnivoreKit/Sources/Models/DataModels/Recommendation.swift
new file mode 100644
index 000000000..279a74268
--- /dev/null
+++ b/apple/OmnivoreKit/Sources/Models/DataModels/Recommendation.swift
@@ -0,0 +1,21 @@
+import CoreData
+import Foundation
+
+public extension Recommendation {
+ var unwrappedID: String { id ?? "" }
+
+ static func lookup(byID recommendationID: String, inContext context: NSManagedObjectContext) -> Recommendation? {
+ let fetchRequest: NSFetchRequest = Recommendation.fetchRequest()
+ fetchRequest.predicate = NSPredicate(
+ format: "id == %@", recommendationID
+ )
+
+ var recommendation: Recommendation?
+
+ context.performAndWait {
+ recommendation = (try? context.fetch(fetchRequest))?.first
+ }
+
+ return recommendation
+ }
+}
diff --git a/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift
index 4de282df4..b1b620be8 100644
--- a/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift
+++ b/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift
@@ -48,6 +48,7 @@ extension DataService {
do {
objectID = try await persistArticleContent(articleProps: fetchResult)
} catch {
+ print("caught article content error: ", error)
var message = "unknown error"
let basicError = (error as? BasicError) ?? BasicError.message(messageText: "unknown error")
if case let BasicError.message(messageText) = basicError {
@@ -230,7 +231,7 @@ extension DataService {
} catch {
// We don't propogate these errors, we just let it pass through so
// the user can attempt to fetch content again.
- print("Error syncUnsyncedArticleContent")
+ print("Error syncUnsyncedArticleContent", error)
}
}
}
diff --git a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift
index c5554e066..33c18d55a 100644
--- a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift
+++ b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift
@@ -684,6 +684,7 @@ extension Objects {
let readAt: [String: DateTime]
let readingProgressAnchorIndex: [String: Int]
let readingProgressPercent: [String: Double]
+ let recommendedBy: [String: [Objects.Recommendation]]
let savedAt: [String: DateTime]
let savedByViewer: [String: Bool]
let shareInfo: [String: Objects.LinkShareInfo]
@@ -806,6 +807,10 @@ extension Objects.Article: Decodable {
if let value = try container.decode(Double?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
+ case "recommendedBy":
+ if let value = try container.decode([Objects.Recommendation]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
case "savedAt":
if let value = try container.decode(DateTime?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
@@ -898,6 +903,7 @@ extension Objects.Article: Decodable {
readAt = map["readAt"]
readingProgressAnchorIndex = map["readingProgressAnchorIndex"]
readingProgressPercent = map["readingProgressPercent"]
+ recommendedBy = map["recommendedBy"]
savedAt = map["savedAt"]
savedByViewer = map["savedByViewer"]
shareInfo = map["shareInfo"]
@@ -1276,6 +1282,22 @@ extension Fields where TypeLock == Objects.Article {
}
}
+ func recommendedBy(selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "recommendedBy",
+ arguments: [],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ return try selection.decode(data: data.recommendedBy[field.alias!])
+ case .mocking:
+ return selection.mock()
+ }
+ }
+
func savedAt() throws -> DateTime {
let field = GraphQLField.leaf(
name: "savedAt",
@@ -8036,6 +8058,137 @@ extension Selection where TypeLock == Never, Type == Never {
typealias IntegrationsSuccess = Selection
}
+extension Objects {
+ struct JoinGroupError {
+ let __typename: TypeName = .joinGroupError
+ let errorCodes: [String: [Enums.JoinGroupErrorCode]]
+
+ enum TypeName: String, Codable {
+ case joinGroupError = "JoinGroupError"
+ }
+ }
+}
+
+extension Objects.JoinGroupError: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "errorCodes":
+ if let value = try container.decode([Enums.JoinGroupErrorCode]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ errorCodes = map["errorCodes"]
+ }
+}
+
+extension Fields where TypeLock == Objects.JoinGroupError {
+ func errorCodes() throws -> [Enums.JoinGroupErrorCode] {
+ let field = GraphQLField.leaf(
+ name: "errorCodes",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.errorCodes[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return []
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias JoinGroupError = Selection
+}
+
+extension Objects {
+ struct JoinGroupSuccess {
+ let __typename: TypeName = .joinGroupSuccess
+ let group: [String: Objects.RecommendationGroup]
+
+ enum TypeName: String, Codable {
+ case joinGroupSuccess = "JoinGroupSuccess"
+ }
+ }
+}
+
+extension Objects.JoinGroupSuccess: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "group":
+ if let value = try container.decode(Objects.RecommendationGroup?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ group = map["group"]
+ }
+}
+
+extension Fields where TypeLock == Objects.JoinGroupSuccess {
+ func group(selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "group",
+ arguments: [],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.group[field.alias!] {
+ return try selection.decode(data: data)
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return selection.mock()
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias JoinGroupSuccess = Selection
+}
+
extension Objects {
struct Label {
let __typename: TypeName = .label
@@ -9517,6 +9670,7 @@ extension Objects {
let generateApiKey: [String: Unions.GenerateApiKeyResult]
let googleLogin: [String: Unions.LoginResult]
let googleSignup: [String: Unions.GoogleSignupResult]
+ let joinGroup: [String: Unions.JoinGroupResult]
let logOut: [String: Unions.LogOutResult]
let mergeHighlight: [String: Unions.MergeHighlightResult]
let moveFilter: [String: Unions.MoveFilterResult]
@@ -9669,6 +9823,10 @@ extension Objects.Mutation: Decodable {
if let value = try container.decode(Unions.GoogleSignupResult?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
+ case "joinGroup":
+ if let value = try container.decode(Unions.JoinGroupResult?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
case "logOut":
if let value = try container.decode(Unions.LogOutResult?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
@@ -9851,6 +10009,7 @@ extension Objects.Mutation: Decodable {
generateApiKey = map["generateApiKey"]
googleLogin = map["googleLogin"]
googleSignup = map["googleSignup"]
+ joinGroup = map["joinGroup"]
logOut = map["logOut"]
mergeHighlight = map["mergeHighlight"]
moveFilter = map["moveFilter"]
@@ -10348,6 +10507,25 @@ extension Fields where TypeLock == Objects.Mutation {
}
}
+ func joinGroup(inviteCode: String, selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "joinGroup",
+ arguments: [Argument(name: "inviteCode", type: "String!", value: inviteCode)],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.joinGroup[field.alias!] {
+ return try selection.decode(data: data)
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return selection.mock()
+ }
+ }
+
func logOut(selection: Selection) throws -> Type {
let field = GraphQLField.composite(
name: "logOut",
@@ -13550,6 +13728,119 @@ extension Selection where TypeLock == Never, Type == Never {
typealias RecommendSuccess = Selection
}
+extension Objects {
+ struct Recommendation {
+ let __typename: TypeName = .recommendation
+ let id: [String: String]
+ let name: [String: String]
+ let recommendedAt: [String: DateTime]
+
+ enum TypeName: String, Codable {
+ case recommendation = "Recommendation"
+ }
+ }
+}
+
+extension Objects.Recommendation: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "id":
+ if let value = try container.decode(String?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "name":
+ if let value = try container.decode(String?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "recommendedAt":
+ if let value = try container.decode(DateTime?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ id = map["id"]
+ name = map["name"]
+ recommendedAt = map["recommendedAt"]
+ }
+}
+
+extension Fields where TypeLock == Objects.Recommendation {
+ func id() throws -> String {
+ let field = GraphQLField.leaf(
+ name: "id",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.id[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return String.mockValue
+ }
+ }
+
+ func name() throws -> String {
+ let field = GraphQLField.leaf(
+ name: "name",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.name[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return String.mockValue
+ }
+ }
+
+ func recommendedAt() throws -> DateTime {
+ let field = GraphQLField.leaf(
+ name: "recommendedAt",
+ arguments: []
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ if let data = data.recommendedAt[field.alias!] {
+ return data
+ }
+ throw HttpError.badpayload
+ case .mocking:
+ return DateTime.mockValue
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias Recommendation = Selection
+}
+
extension Objects {
struct RecommendationGroup {
let __typename: TypeName = .recommendationGroup
@@ -15180,6 +15471,7 @@ extension Objects {
let readAt: [String: DateTime]
let readingProgressAnchorIndex: [String: Int]
let readingProgressPercent: [String: Double]
+ let recommendedBy: [String: [Objects.Recommendation]]
let savedAt: [String: DateTime]
let shortId: [String: String]
let siteIcon: [String: String]
@@ -15292,6 +15584,10 @@ extension Objects.SearchItem: Decodable {
if let value = try container.decode(Double?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
}
+ case "recommendedBy":
+ if let value = try container.decode([Objects.Recommendation]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
case "savedAt":
if let value = try container.decode(DateTime?.self, forKey: codingKey) {
map.set(key: field, hash: alias, value: value as Any)
@@ -15374,6 +15670,7 @@ extension Objects.SearchItem: Decodable {
readAt = map["readAt"]
readingProgressAnchorIndex = map["readingProgressAnchorIndex"]
readingProgressPercent = map["readingProgressPercent"]
+ recommendedBy = map["recommendedBy"]
savedAt = map["savedAt"]
shortId = map["shortId"]
siteIcon = map["siteIcon"]
@@ -15714,6 +16011,22 @@ extension Fields where TypeLock == Objects.SearchItem {
}
}
+ func recommendedBy(selection: Selection) throws -> Type {
+ let field = GraphQLField.composite(
+ name: "recommendedBy",
+ arguments: [],
+ selection: selection.selection
+ )
+ select(field)
+
+ switch response {
+ case let .decoding(data):
+ return try selection.decode(data: data.recommendedBy[field.alias!])
+ case .mocking:
+ return selection.mock()
+ }
+ }
+
func savedAt() throws -> DateTime {
let field = GraphQLField.leaf(
name: "savedAt",
@@ -24526,6 +24839,80 @@ extension Selection where TypeLock == Never, Type == Never {
typealias IntegrationsResult = Selection
}
+extension Unions {
+ struct JoinGroupResult {
+ let __typename: TypeName
+ let errorCodes: [String: [Enums.JoinGroupErrorCode]]
+ let group: [String: Objects.RecommendationGroup]
+
+ enum TypeName: String, Codable {
+ case joinGroupError = "JoinGroupError"
+ case joinGroupSuccess = "JoinGroupSuccess"
+ }
+ }
+}
+
+extension Unions.JoinGroupResult: Decodable {
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ var map = HashMap()
+ for codingKey in container.allKeys {
+ if codingKey.isTypenameKey { continue }
+
+ let alias = codingKey.stringValue
+ let field = GraphQLField.getFieldNameFromAlias(alias)
+
+ switch field {
+ case "errorCodes":
+ if let value = try container.decode([Enums.JoinGroupErrorCode]?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ case "group":
+ if let value = try container.decode(Objects.RecommendationGroup?.self, forKey: codingKey) {
+ map.set(key: field, hash: alias, value: value as Any)
+ }
+ default:
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Unknown key \(field)."
+ )
+ )
+ }
+ }
+
+ __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!)
+
+ errorCodes = map["errorCodes"]
+ group = map["group"]
+ }
+}
+
+extension Fields where TypeLock == Unions.JoinGroupResult {
+ func on(joinGroupError: Selection, joinGroupSuccess: Selection) throws -> Type {
+ select([GraphQLField.fragment(type: "JoinGroupError", selection: joinGroupError.selection), GraphQLField.fragment(type: "JoinGroupSuccess", selection: joinGroupSuccess.selection)])
+
+ switch response {
+ case let .decoding(data):
+ switch data.__typename {
+ case .joinGroupError:
+ let data = Objects.JoinGroupError(errorCodes: data.errorCodes)
+ return try joinGroupError.decode(data: data)
+ case .joinGroupSuccess:
+ let data = Objects.JoinGroupSuccess(group: data.group)
+ return try joinGroupSuccess.decode(data: data)
+ }
+ case .mocking:
+ return joinGroupError.mock()
+ }
+ }
+}
+
+extension Selection where TypeLock == Never, Type == Never {
+ typealias JoinGroupResult = Selection
+}
+
extension Unions {
struct LabelsResult {
let __typename: TypeName
@@ -28541,6 +28928,17 @@ extension Enums {
}
}
+extension Enums {
+ /// JoinGroupErrorCode
+ enum JoinGroupErrorCode: String, CaseIterable, Codable {
+ case badRequest = "BAD_REQUEST"
+
+ case notFound = "NOT_FOUND"
+
+ case unauthorized = "UNAUTHORIZED"
+ }
+}
+
extension Enums {
/// LabelsErrorCode
enum LabelsErrorCode: String, CaseIterable, Codable {
diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift
index f8b427b37..8b00c363e 100644
--- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift
+++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/SavePage.swift
@@ -59,6 +59,7 @@ public extension DataService {
}
}
case let .failure(error):
+ print("RESULT: ", result)
continuation.resume(throwing: SaveArticleError.make(from: error))
}
}
@@ -72,6 +73,7 @@ extension SaveArticleError {
case .network, .timeout:
return .network
case .badpayload, .badURL, .badstatus, .cancelled:
+ print("HTTP ERROR", httpError)
return .unknown(description: httpError.localizedDescription)
}
}
diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift
index 46c4160db..bf7f5f639 100644
--- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift
+++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift
@@ -44,6 +44,7 @@ extension DataService {
contentReader: try $0.contentReader().rawValue,
originalHtml: nil,
language: try $0.language(),
+ recommendedBy: try $0.recommendedBy(selection: recommendationSelection.list.nullable) ?? [],
labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? []
),
htmlContent: try $0.content(),
diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift
index ce5d1fb7e..6bb543079 100644
--- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift
+++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift
@@ -224,6 +224,14 @@ extension DataService {
}
}
+let recommendationSelection = Selection.Recommendation {
+ InternalRecommendation(
+ id: try $0.id(),
+ name: try $0.name(),
+ recommendedAt: try $0.recommendedAt().value ?? Date()
+ )
+}
+
private let libraryArticleSelection = Selection.Article {
InternalLinkedItem(
id: try $0.id(),
@@ -249,6 +257,7 @@ private let libraryArticleSelection = Selection.Article {
contentReader: try $0.contentReader().rawValue,
originalHtml: nil,
language: try $0.language(),
+ recommendedBy: try $0.recommendedBy(selection: recommendationSelection.list.nullable) ?? [],
labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? []
)
}
@@ -286,6 +295,7 @@ private let searchItemSelection = Selection.SearchItem {
contentReader: try $0.contentReader().rawValue,
originalHtml: nil,
language: try $0.language(),
+ recommendedBy: try $0.recommendedBy(selection: recommendationSelection.list.nullable) ?? [],
labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? []
)
}
diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift
index 1950ef3c6..77ee4ae91 100644
--- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift
+++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift
@@ -26,6 +26,7 @@ struct InternalLinkedItem {
let contentReader: String?
let originalHtml: String?
let language: String?
+ let recommendedBy: [InternalRecommendation]
var labels: [InternalLinkedItemLabel]
var isPDF: Bool {
@@ -72,6 +73,14 @@ struct InternalLinkedItem {
linkedItem.addToLabels(label.asManagedObject(inContext: context))
}
+ if let existingRecommendation = linkedItem.recommendedBy {
+ linkedItem.removeFromRecommendedBy(existingRecommendation)
+ }
+
+ for recommendation in recommendedBy {
+ linkedItem.addToRecommendedBy(recommendation.asManagedObject(inContext: context))
+ }
+
return linkedItem
}
}
@@ -133,6 +142,7 @@ extension JSONArticle {
contentReader: contentReader,
originalHtml: nil,
language: language,
+ recommendedBy: [], // TODO:
labels: []
)
diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendation.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendation.swift
new file mode 100644
index 000000000..c706d7014
--- /dev/null
+++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendation.swift
@@ -0,0 +1,36 @@
+import CoreData
+import Foundation
+import Models
+
+public struct InternalRecommendation: Encodable {
+ let id: String
+ let name: String
+ let recommendedAt: Date?
+
+ func asManagedObject(inContext context: NSManagedObjectContext) -> Recommendation {
+ let existing = Recommendation.lookup(byID: id, inContext: context)
+ let recommendation = existing ?? Recommendation(entity: Recommendation.entity(), insertInto: context)
+ recommendation.id = id
+ recommendation.name = name
+ recommendation.recommendedAt = recommendedAt
+ return recommendation
+ }
+
+ public static func make(_ recommendations: NSSet?) -> [InternalRecommendation] {
+ recommendations?
+ .compactMap { recommendation in
+ if let recommendation = recommendation as? Recommendation,
+ let id = recommendation.id,
+ let name = recommendation.name,
+ let recommendedAt = recommendation.recommendedAt
+ {
+ return InternalRecommendation(
+ id: id,
+ name: name,
+ recommendedAt: recommendedAt
+ )
+ }
+ return nil
+ } ?? []
+ }
+}
diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/HomeFeedCardView.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/HomeFeedCardView.swift
index 42af43b98..1a7d04bd6 100644
--- a/apple/OmnivoreKit/Sources/Views/FeedItem/HomeFeedCardView.swift
+++ b/apple/OmnivoreKit/Sources/Views/FeedItem/HomeFeedCardView.swift
@@ -86,6 +86,21 @@ public struct FeedCard: View {
}
#endif
}
+
+ if let recommendedBy = item.recommendedBy, recommendedBy.count > 0 {
+ let str = recommendedBy.reduce("") { str, item in
+ if let item = item as? Recommendation, let name = item.name {
+ return str + name
+ }
+ return str
+ }
+ HStack {
+ Text("Recommended in \(str)")
+ .font(.appCaption)
+ .frame(alignment: .leading)
+ Spacer()
+ }
+ }
}
.padding(.top, 0)
.padding(.bottom, 8)