UI for recommendations
This commit is contained in:
@ -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: {
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -55,6 +55,7 @@
|
||||
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="highlights" toMany="YES" deletionRule="Cascade" destinationEntity="Highlight" inverseName="linkedItem" inverseEntity="Highlight"/>
|
||||
<relationship name="labels" toMany="YES" deletionRule="Nullify" destinationEntity="LinkedItemLabel" inverseName="linkedItems" inverseEntity="LinkedItemLabel"/>
|
||||
<relationship name="recommendedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Recommendation" inverseName="linkedItem" inverseEntity="Recommendation"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="id"/>
|
||||
@ -91,6 +92,12 @@
|
||||
<attribute name="savedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="term" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
<entity name="Recommendation" representedClassName="Recommendation" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="id" optional="YES" attributeType="String"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="recommendedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="linkedItem" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LinkedItem" inverseName="recommendedBy" inverseEntity="LinkedItem"/>
|
||||
</entity>
|
||||
<entity name="RecommendationGroup" representedClassName="RecommendationGroup" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="id" optional="YES" attributeType="String"/>
|
||||
|
||||
@ -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<Models.Recommendation> = Recommendation.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(
|
||||
format: "id == %@", recommendationID
|
||||
)
|
||||
|
||||
var recommendation: Recommendation?
|
||||
|
||||
context.performAndWait {
|
||||
recommendation = (try? context.fetch(fetchRequest))?.first
|
||||
}
|
||||
|
||||
return recommendation
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Type>(selection: Selection<Type, [Objects.Recommendation]?>) 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<T> = Selection<T, Objects.IntegrationsSuccess>
|
||||
}
|
||||
|
||||
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<T> = Selection<T, Objects.JoinGroupError>
|
||||
}
|
||||
|
||||
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<Type>(selection: Selection<Type, Objects.RecommendationGroup>) 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<T> = Selection<T, Objects.JoinGroupSuccess>
|
||||
}
|
||||
|
||||
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<Type>(inviteCode: String, selection: Selection<Type, Unions.JoinGroupResult>) 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<Type>(selection: Selection<Type, Unions.LogOutResult>) throws -> Type {
|
||||
let field = GraphQLField.composite(
|
||||
name: "logOut",
|
||||
@ -13550,6 +13728,119 @@ extension Selection where TypeLock == Never, Type == Never {
|
||||
typealias RecommendSuccess<T> = Selection<T, Objects.RecommendSuccess>
|
||||
}
|
||||
|
||||
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<T> = Selection<T, Objects.Recommendation>
|
||||
}
|
||||
|
||||
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<Type>(selection: Selection<Type, [Objects.Recommendation]?>) 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<T> = Selection<T, Unions.IntegrationsResult>
|
||||
}
|
||||
|
||||
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<Type>(joinGroupError: Selection<Type, Objects.JoinGroupError>, joinGroupSuccess: Selection<Type, Objects.JoinGroupSuccess>) 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<T> = Selection<T, Unions.JoinGroupResult>
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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) ?? []
|
||||
)
|
||||
}
|
||||
|
||||
@ -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: []
|
||||
)
|
||||
|
||||
|
||||
@ -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
|
||||
} ?? []
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
Reference in New Issue
Block a user