mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Rename recommendedBy to recommendations
This commit is contained in:
parent
ba8cd22611
commit
a8df439297
21 changed files with 453 additions and 109 deletions
|
|
@ -10,6 +10,7 @@ import Views
|
|||
@Published var selectedGroups = [String]()
|
||||
@Published var isRunning = false
|
||||
@Published var showError = false
|
||||
@Published var note: String?
|
||||
|
||||
let pageID: String
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ import Views
|
|||
isRunning = true
|
||||
|
||||
do {
|
||||
try await dataService.recommendPage(pageID: pageID, groupIDs: selectedGroups)
|
||||
try await dataService.recommendPage(pageID: pageID, groupIDs: selectedGroups, note: note)
|
||||
} catch {
|
||||
showError = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +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"/>
|
||||
<relationship name="recommendations" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Recommendation" inverseName="linkedItem" inverseEntity="Recommendation"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="id"/>
|
||||
|
|
@ -96,7 +96,8 @@
|
|||
<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"/>
|
||||
<relationship name="linkedItem" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LinkedItem" inverseName="recommendations" inverseEntity="LinkedItem"/>
|
||||
<relationship name="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserProfile"/>
|
||||
</entity>
|
||||
<entity name="RecommendationGroup" representedClassName="RecommendationGroup" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ public class PersistentContainer: NSPersistentContainer {
|
|||
#endif
|
||||
let appGroupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupID)
|
||||
let appGroupContainerURL = appGroupContainer?.appendingPathComponent("store.sqlite")
|
||||
print("starting with sqlite container", appGroupContainerURL)
|
||||
|
||||
container.persistentStoreDescriptions.first!.url = appGroupContainerURL
|
||||
|
||||
|
|
|
|||
|
|
@ -684,7 +684,7 @@ extension Objects {
|
|||
let readAt: [String: DateTime]
|
||||
let readingProgressAnchorIndex: [String: Int]
|
||||
let readingProgressPercent: [String: Double]
|
||||
let recommendedBy: [String: [Objects.Recommendation]]
|
||||
let recommendations: [String: [Objects.Recommendation]]
|
||||
let savedAt: [String: DateTime]
|
||||
let savedByViewer: [String: Bool]
|
||||
let shareInfo: [String: Objects.LinkShareInfo]
|
||||
|
|
@ -807,7 +807,7 @@ 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":
|
||||
case "recommendations":
|
||||
if let value = try container.decode([Objects.Recommendation]?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
|
|
@ -903,7 +903,7 @@ extension Objects.Article: Decodable {
|
|||
readAt = map["readAt"]
|
||||
readingProgressAnchorIndex = map["readingProgressAnchorIndex"]
|
||||
readingProgressPercent = map["readingProgressPercent"]
|
||||
recommendedBy = map["recommendedBy"]
|
||||
recommendations = map["recommendations"]
|
||||
savedAt = map["savedAt"]
|
||||
savedByViewer = map["savedByViewer"]
|
||||
shareInfo = map["shareInfo"]
|
||||
|
|
@ -1282,9 +1282,9 @@ extension Fields where TypeLock == Objects.Article {
|
|||
}
|
||||
}
|
||||
|
||||
func recommendedBy<Type>(selection: Selection<Type, [Objects.Recommendation]?>) throws -> Type {
|
||||
func recommendations<Type>(selection: Selection<Type, [Objects.Recommendation]?>) throws -> Type {
|
||||
let field = GraphQLField.composite(
|
||||
name: "recommendedBy",
|
||||
name: "recommendations",
|
||||
arguments: [],
|
||||
selection: selection.selection
|
||||
)
|
||||
|
|
@ -1292,7 +1292,7 @@ extension Fields where TypeLock == Objects.Article {
|
|||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return try selection.decode(data: data.recommendedBy[field.alias!])
|
||||
return try selection.decode(data: data.recommendations[field.alias!])
|
||||
case .mocking:
|
||||
return selection.mock()
|
||||
}
|
||||
|
|
@ -13733,7 +13733,9 @@ extension Objects {
|
|||
let __typename: TypeName = .recommendation
|
||||
let id: [String: String]
|
||||
let name: [String: String]
|
||||
let note: [String: String]
|
||||
let recommendedAt: [String: DateTime]
|
||||
let user: [String: Objects.RecommendingUser]
|
||||
|
||||
enum TypeName: String, Codable {
|
||||
case recommendation = "Recommendation"
|
||||
|
|
@ -13761,10 +13763,18 @@ extension Objects.Recommendation: Decodable {
|
|||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "note":
|
||||
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)
|
||||
}
|
||||
case "user":
|
||||
if let value = try container.decode(Objects.RecommendingUser?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
default:
|
||||
throw DecodingError.dataCorrupted(
|
||||
DecodingError.Context(
|
||||
|
|
@ -13777,7 +13787,9 @@ extension Objects.Recommendation: Decodable {
|
|||
|
||||
id = map["id"]
|
||||
name = map["name"]
|
||||
note = map["note"]
|
||||
recommendedAt = map["recommendedAt"]
|
||||
user = map["user"]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -13818,6 +13830,21 @@ extension Fields where TypeLock == Objects.Recommendation {
|
|||
}
|
||||
}
|
||||
|
||||
func note() throws -> String? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "note",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return data.note[field.alias!]
|
||||
case .mocking:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func recommendedAt() throws -> DateTime {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "recommendedAt",
|
||||
|
|
@ -13835,6 +13862,22 @@ extension Fields where TypeLock == Objects.Recommendation {
|
|||
return DateTime.mockValue
|
||||
}
|
||||
}
|
||||
|
||||
func user<Type>(selection: Selection<Type, Objects.RecommendingUser?>) throws -> Type {
|
||||
let field = GraphQLField.composite(
|
||||
name: "user",
|
||||
arguments: [],
|
||||
selection: selection.selection
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return try selection.decode(data: data.user[field.alias!])
|
||||
case .mocking:
|
||||
return selection.mock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Selection where TypeLock == Never, Type == Never {
|
||||
|
|
@ -14052,6 +14095,140 @@ extension Selection where TypeLock == Never, Type == Never {
|
|||
typealias RecommendationGroup<T> = Selection<T, Objects.RecommendationGroup>
|
||||
}
|
||||
|
||||
extension Objects {
|
||||
struct RecommendingUser {
|
||||
let __typename: TypeName = .recommendingUser
|
||||
let name: [String: String]
|
||||
let profileImageUrl: [String: String]
|
||||
let userId: [String: String]
|
||||
let username: [String: String]
|
||||
|
||||
enum TypeName: String, Codable {
|
||||
case recommendingUser = "RecommendingUser"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Objects.RecommendingUser: 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 "name":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "profileImageUrl":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "userId":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "username":
|
||||
if let value = try container.decode(String?.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)."
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
name = map["name"]
|
||||
profileImageUrl = map["profileImageUrl"]
|
||||
userId = map["userId"]
|
||||
username = map["username"]
|
||||
}
|
||||
}
|
||||
|
||||
extension Fields where TypeLock == Objects.RecommendingUser {
|
||||
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 profileImageUrl() throws -> String? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "profileImageURL",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return data.profileImageUrl[field.alias!]
|
||||
case .mocking:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func userId() throws -> String {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "userId",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
if let data = data.userId[field.alias!] {
|
||||
return data
|
||||
}
|
||||
throw HttpError.badpayload
|
||||
case .mocking:
|
||||
return String.mockValue
|
||||
}
|
||||
}
|
||||
|
||||
func username() throws -> String {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "username",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
if let data = data.username[field.alias!] {
|
||||
return data
|
||||
}
|
||||
throw HttpError.badpayload
|
||||
case .mocking:
|
||||
return String.mockValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Selection where TypeLock == Never, Type == Never {
|
||||
typealias RecommendingUser<T> = Selection<T, Objects.RecommendingUser>
|
||||
}
|
||||
|
||||
extension Objects {
|
||||
struct Reminder {
|
||||
let __typename: TypeName = .reminder
|
||||
|
|
@ -15471,7 +15648,7 @@ extension Objects {
|
|||
let readAt: [String: DateTime]
|
||||
let readingProgressAnchorIndex: [String: Int]
|
||||
let readingProgressPercent: [String: Double]
|
||||
let recommendedBy: [String: [Objects.Recommendation]]
|
||||
let recommendations: [String: [Objects.Recommendation]]
|
||||
let savedAt: [String: DateTime]
|
||||
let shortId: [String: String]
|
||||
let siteIcon: [String: String]
|
||||
|
|
@ -15584,7 +15761,7 @@ 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":
|
||||
case "recommendations":
|
||||
if let value = try container.decode([Objects.Recommendation]?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
|
|
@ -15670,7 +15847,7 @@ extension Objects.SearchItem: Decodable {
|
|||
readAt = map["readAt"]
|
||||
readingProgressAnchorIndex = map["readingProgressAnchorIndex"]
|
||||
readingProgressPercent = map["readingProgressPercent"]
|
||||
recommendedBy = map["recommendedBy"]
|
||||
recommendations = map["recommendations"]
|
||||
savedAt = map["savedAt"]
|
||||
shortId = map["shortId"]
|
||||
siteIcon = map["siteIcon"]
|
||||
|
|
@ -16011,9 +16188,9 @@ extension Fields where TypeLock == Objects.SearchItem {
|
|||
}
|
||||
}
|
||||
|
||||
func recommendedBy<Type>(selection: Selection<Type, [Objects.Recommendation]?>) throws -> Type {
|
||||
func recommendations<Type>(selection: Selection<Type, [Objects.Recommendation]?>) throws -> Type {
|
||||
let field = GraphQLField.composite(
|
||||
name: "recommendedBy",
|
||||
name: "recommendations",
|
||||
arguments: [],
|
||||
selection: selection.selection
|
||||
)
|
||||
|
|
@ -16021,7 +16198,7 @@ extension Fields where TypeLock == Objects.SearchItem {
|
|||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return try selection.decode(data: data.recommendedBy[field.alias!])
|
||||
return try selection.decode(data: data.recommendations[field.alias!])
|
||||
case .mocking:
|
||||
return selection.mock()
|
||||
}
|
||||
|
|
@ -30128,16 +30305,20 @@ extension InputObjects {
|
|||
struct RecommendInput: Encodable, Hashable {
|
||||
var groupIds: [String]
|
||||
|
||||
var note: OptionalArgument<String> = .absent()
|
||||
|
||||
var pageId: String
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(groupIds, forKey: .groupIds)
|
||||
if note.hasValue { try container.encode(note, forKey: .note) }
|
||||
try container.encode(pageId, forKey: .pageId)
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case groupIds
|
||||
case note
|
||||
case pageId
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import Models
|
|||
import SwiftGraphQL
|
||||
|
||||
public extension DataService {
|
||||
func recommendPage(pageID: String, groupIDs: [String]) async throws {
|
||||
func recommendPage(pageID: String, groupIDs: [String], note: String?) async throws {
|
||||
enum MutationResult {
|
||||
case saved(taskNames: [String])
|
||||
case error(errorMessage: String)
|
||||
|
|
@ -21,7 +21,7 @@ public extension DataService {
|
|||
|
||||
let mutation = Selection.Mutation {
|
||||
try $0.recommend(
|
||||
input: .init(groupIds: groupIDs, pageId: pageID),
|
||||
input: .init(groupIds: groupIDs, note: OptionalArgument(note), pageId: pageID),
|
||||
selection: selection
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ extension DataService {
|
|||
contentReader: try $0.contentReader().rawValue,
|
||||
originalHtml: nil,
|
||||
language: try $0.language(),
|
||||
recommendedBy: try $0.recommendedBy(selection: recommendationSelection.list.nullable) ?? [],
|
||||
recommendations: try $0.recommendations(selection: recommendationSelection.list.nullable) ?? [],
|
||||
labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? []
|
||||
),
|
||||
htmlContent: try $0.content(),
|
||||
|
|
|
|||
|
|
@ -224,12 +224,38 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
let recommendationSelection = Selection.Recommendation {
|
||||
InternalRecommendation(
|
||||
id: try $0.id(),
|
||||
name: try $0.name(),
|
||||
recommendedAt: try $0.recommendedAt().value ?? Date()
|
||||
)
|
||||
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 []
|
||||
}
|
||||
|
||||
private let libraryArticleSelection = Selection.Article {
|
||||
|
|
@ -257,7 +283,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) ?? [],
|
||||
recommendations: try $0.recommendations(selection: recommendationSelection.list.nullable) ?? emptyrecommended(),
|
||||
labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? []
|
||||
)
|
||||
}
|
||||
|
|
@ -295,7 +321,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) ?? [],
|
||||
recommendations: try $0.recommendations(selection: recommendationSelection.list.nullable) ?? [],
|
||||
labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? []
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ struct InternalLinkedItem {
|
|||
let contentReader: String?
|
||||
let originalHtml: String?
|
||||
let language: String?
|
||||
let recommendedBy: [InternalRecommendation]
|
||||
let recommendations: [InternalRecommendation]
|
||||
var labels: [InternalLinkedItemLabel]
|
||||
|
||||
var isPDF: Bool {
|
||||
|
|
@ -73,12 +73,12 @@ struct InternalLinkedItem {
|
|||
linkedItem.addToLabels(label.asManagedObject(inContext: context))
|
||||
}
|
||||
|
||||
if let existingRecommendation = linkedItem.recommendedBy {
|
||||
linkedItem.removeFromRecommendedBy(existingRecommendation)
|
||||
if let existingRecommendation = linkedItem.recommendations {
|
||||
linkedItem.removeFromRecommendations(existingRecommendation)
|
||||
}
|
||||
|
||||
for recommendation in recommendedBy {
|
||||
linkedItem.addToRecommendedBy(recommendation.asManagedObject(inContext: context))
|
||||
for recommendation in recommendations {
|
||||
linkedItem.addToRecommendations(recommendation.asManagedObject(inContext: context))
|
||||
}
|
||||
|
||||
return linkedItem
|
||||
|
|
@ -142,7 +142,7 @@ extension JSONArticle {
|
|||
contentReader: contentReader,
|
||||
originalHtml: nil,
|
||||
language: language,
|
||||
recommendedBy: [], // TODO:
|
||||
recommendations: [], // TODO:
|
||||
labels: []
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ import CoreData
|
|||
import Foundation
|
||||
import Models
|
||||
|
||||
public struct InternalRecommendation: Encodable {
|
||||
public struct InternalRecommendation {
|
||||
let id: String
|
||||
let name: String
|
||||
let recommendedAt: Date?
|
||||
let user: InternalUserProfile?
|
||||
let recommendedAt: Date
|
||||
|
||||
func asManagedObject(inContext context: NSManagedObjectContext) -> Recommendation {
|
||||
let existing = Recommendation.lookup(byID: id, inContext: context)
|
||||
|
|
@ -13,6 +14,7 @@ public struct InternalRecommendation: Encodable {
|
|||
recommendation.id = id
|
||||
recommendation.name = name
|
||||
recommendation.recommendedAt = recommendedAt
|
||||
recommendation.user = user?.asManagedObject(inContext: context)
|
||||
return recommendation
|
||||
}
|
||||
|
||||
|
|
@ -27,6 +29,7 @@ public struct InternalRecommendation: Encodable {
|
|||
return InternalRecommendation(
|
||||
id: id,
|
||||
name: name,
|
||||
user: InternalUserProfile.makeSingle(recommendation.user),
|
||||
recommendedAt: recommendedAt
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,22 @@ public struct InternalUserProfile: Identifiable {
|
|||
return userProfile
|
||||
}
|
||||
|
||||
public static func makeSingle(_ user: UserProfile?) -> InternalUserProfile? {
|
||||
if let user = user,
|
||||
let userID = user.userID,
|
||||
let name = user.name,
|
||||
let username = user.username
|
||||
{
|
||||
return InternalUserProfile(
|
||||
userID: userID,
|
||||
name: name,
|
||||
username: username,
|
||||
profileImageURL: user.profileImageURL
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public static func make(_ users: NSSet?) -> [InternalUserProfile] {
|
||||
users?
|
||||
.compactMap { user in
|
||||
|
|
|
|||
|
|
@ -87,15 +87,21 @@ public struct FeedCard: View {
|
|||
#endif
|
||||
}
|
||||
|
||||
if let recommendedBy = item.recommendedBy, recommendedBy.count > 0 {
|
||||
let str = recommendedBy.reduce("") { str, item in
|
||||
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
|
||||
}
|
||||
HStack {
|
||||
Text("Recommended in \(str)")
|
||||
Text("Recommended by \(byStr) in \(inStr)")
|
||||
.font(.appCaption)
|
||||
.frame(alignment: .leading)
|
||||
Spacer()
|
||||
|
|
|
|||
|
|
@ -13,19 +13,19 @@ export const addRecommendation = async (
|
|||
url: page.url,
|
||||
})
|
||||
if (existingPage) {
|
||||
if (existingPage.recommendedBy?.includes(recommendation)) {
|
||||
if (existingPage.recommendations?.includes(recommendation)) {
|
||||
return existingPage._id
|
||||
}
|
||||
|
||||
// update recommendedBy in the existing page
|
||||
const recommendedBy = (existingPage.recommendedBy || []).concat(
|
||||
const recommendations = (existingPage.recommendations || []).concat(
|
||||
recommendation
|
||||
)
|
||||
|
||||
await updatePage(
|
||||
existingPage.id,
|
||||
{
|
||||
recommendedBy,
|
||||
recommendations,
|
||||
},
|
||||
ctx
|
||||
)
|
||||
|
|
@ -36,7 +36,7 @@ export const addRecommendation = async (
|
|||
const newPage: Page = {
|
||||
...page,
|
||||
id: '',
|
||||
recommendedBy: [recommendation],
|
||||
recommendations: [recommendation],
|
||||
userId: ctx.uid,
|
||||
readingProgressPercent: 0,
|
||||
readingProgressAnchorIndex: 0,
|
||||
|
|
|
|||
|
|
@ -249,7 +249,7 @@ export interface Page {
|
|||
readAt?: Date
|
||||
listenedAt?: Date
|
||||
wordsCount?: number
|
||||
recommendedBy?: Recommendation[]
|
||||
recommendations?: Recommendation[]
|
||||
}
|
||||
|
||||
export interface SearchItem {
|
||||
|
|
@ -282,7 +282,7 @@ export interface SearchItem {
|
|||
wordsCount?: number
|
||||
siteName?: string
|
||||
siteIcon?: string
|
||||
recommendedBy?: Recommendation[]
|
||||
recommendations?: Recommendation[]
|
||||
}
|
||||
|
||||
const keys = ['_id', 'url', 'slug', 'userId', 'uploadFileId', 'state'] as const
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ export type Article = {
|
|||
readAt?: Maybe<Scalars['Date']>;
|
||||
readingProgressAnchorIndex: Scalars['Int'];
|
||||
readingProgressPercent: Scalars['Float'];
|
||||
recommendedBy?: Maybe<Array<Recommendation>>;
|
||||
recommendations?: Maybe<Array<Recommendation>>;
|
||||
savedAt: Scalars['Date'];
|
||||
savedByViewer?: Maybe<Scalars['Boolean']>;
|
||||
shareInfo?: Maybe<LinkShareInfo>;
|
||||
|
|
@ -1804,7 +1804,7 @@ export type Recommendation = {
|
|||
name: Scalars['String'];
|
||||
note?: Maybe<Scalars['String']>;
|
||||
recommendedAt: Scalars['Date'];
|
||||
user: RecommendingUser;
|
||||
user?: Maybe<RecommendingUser>;
|
||||
};
|
||||
|
||||
export type RecommendationGroup = {
|
||||
|
|
@ -2057,7 +2057,7 @@ export type SearchItem = {
|
|||
readAt?: Maybe<Scalars['Date']>;
|
||||
readingProgressAnchorIndex: Scalars['Int'];
|
||||
readingProgressPercent: Scalars['Float'];
|
||||
recommendedBy?: Maybe<Array<Recommendation>>;
|
||||
recommendations?: Maybe<Array<Recommendation>>;
|
||||
savedAt: Scalars['Date'];
|
||||
shortId?: Maybe<Scalars['String']>;
|
||||
siteIcon?: Maybe<Scalars['String']>;
|
||||
|
|
@ -3889,7 +3889,7 @@ export type ArticleResolvers<ContextType = ResolverContext, ParentType extends R
|
|||
readAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
|
||||
readingProgressAnchorIndex?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
readingProgressPercent?: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
|
||||
recommendedBy?: Resolver<Maybe<Array<ResolversTypes['Recommendation']>>, ParentType, ContextType>;
|
||||
recommendations?: Resolver<Maybe<Array<ResolversTypes['Recommendation']>>, ParentType, ContextType>;
|
||||
savedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
|
||||
savedByViewer?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
|
||||
shareInfo?: Resolver<Maybe<ResolversTypes['LinkShareInfo']>, ParentType, ContextType>;
|
||||
|
|
@ -4851,7 +4851,7 @@ export type RecommendationResolvers<ContextType = ResolverContext, ParentType ex
|
|||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
note?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
recommendedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
|
||||
user?: Resolver<ResolversTypes['RecommendingUser'], ParentType, ContextType>;
|
||||
user?: Resolver<Maybe<ResolversTypes['RecommendingUser']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
|
|
@ -5016,7 +5016,7 @@ export type SearchItemResolvers<ContextType = ResolverContext, ParentType extend
|
|||
readAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
|
||||
readingProgressAnchorIndex?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
readingProgressPercent?: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
|
||||
recommendedBy?: Resolver<Maybe<Array<ResolversTypes['Recommendation']>>, ParentType, ContextType>;
|
||||
recommendations?: Resolver<Maybe<Array<ResolversTypes['Recommendation']>>, ParentType, ContextType>;
|
||||
savedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
|
||||
shortId?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
siteIcon?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ type Article {
|
|||
readAt: Date
|
||||
readingProgressAnchorIndex: Int!
|
||||
readingProgressPercent: Float!
|
||||
recommendedBy: [Recommendation!]
|
||||
recommendations: [Recommendation!]
|
||||
savedAt: Date!
|
||||
savedByViewer: Boolean
|
||||
shareInfo: LinkShareInfo
|
||||
|
|
@ -1265,7 +1265,7 @@ type Recommendation {
|
|||
name: String!
|
||||
note: String
|
||||
recommendedAt: Date!
|
||||
user: RecommendingUser!
|
||||
user: RecommendingUser
|
||||
}
|
||||
|
||||
type RecommendationGroup {
|
||||
|
|
@ -1498,7 +1498,7 @@ type SearchItem {
|
|||
readAt: Date
|
||||
readingProgressAnchorIndex: Int!
|
||||
readingProgressPercent: Float!
|
||||
recommendedBy: [Recommendation!]
|
||||
recommendations: [Recommendation!]
|
||||
savedAt: Date!
|
||||
shortId: String
|
||||
siteIcon: String
|
||||
|
|
|
|||
|
|
@ -1015,6 +1015,7 @@ export const updatesSinceResolver = authorized<
|
|||
}
|
||||
})
|
||||
|
||||
console.log('edges', JSON.stringify(edges))
|
||||
return {
|
||||
edges,
|
||||
pageInfo: {
|
||||
|
|
|
|||
|
|
@ -185,7 +185,8 @@ export const recommendResolver = authorized<
|
|||
member.user.id,
|
||||
page.id,
|
||||
{
|
||||
...group,
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
note: input.note ?? null,
|
||||
user: {
|
||||
userId: user.id,
|
||||
|
|
|
|||
|
|
@ -384,7 +384,7 @@ const schema = gql`
|
|||
state: ArticleSavingRequestStatus
|
||||
language: String
|
||||
readAt: Date
|
||||
recommendedBy: [Recommendation!]
|
||||
recommendations: [Recommendation!]
|
||||
}
|
||||
|
||||
# Query: article
|
||||
|
|
@ -1531,7 +1531,7 @@ const schema = gql`
|
|||
savedAt: Date!
|
||||
highlights: [Highlight!]
|
||||
siteIcon: String
|
||||
recommendedBy: [Recommendation!]
|
||||
recommendations: [Recommendation!]
|
||||
}
|
||||
|
||||
type SearchItemEdge {
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@
|
|||
"wordsCount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"recommendedBy": {
|
||||
"recommendations": {
|
||||
"type": "nested",
|
||||
"properties": {
|
||||
"id": {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import chalk from 'chalk'
|
|||
import { Client } from '@elastic/elasticsearch'
|
||||
import { readFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { print } from 'graphql'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
|
|
@ -109,6 +110,86 @@ const createIndex = async (): Promise<void> => {
|
|||
})
|
||||
}
|
||||
|
||||
const reIndex = async (): Promise<void> => {
|
||||
const newPages = INDEX_NAME + '-002'
|
||||
// // create index
|
||||
// try {
|
||||
// await esClient.indices.delete({
|
||||
// index: newPages,
|
||||
// })
|
||||
// } catch (e) {
|
||||
// console.log('001: ', e)
|
||||
// }
|
||||
|
||||
// try {
|
||||
// await esClient.indices.delete({
|
||||
// index: 'pages-temp',
|
||||
// })
|
||||
// } catch (e) {
|
||||
// console.log('002: ', e)
|
||||
// }
|
||||
|
||||
// try {
|
||||
// await esClient.indices.delete({
|
||||
// index: 'pages-temp-001',
|
||||
// })
|
||||
// } catch (e) {
|
||||
// console.log('003: ', e)
|
||||
// }
|
||||
|
||||
// try {
|
||||
// await esClient.indices.delete({
|
||||
// index: 'pages-temp',
|
||||
// })
|
||||
// } catch (e) {
|
||||
// console.log('002: ', e)
|
||||
// }
|
||||
|
||||
try {
|
||||
log('getting pages alias:')
|
||||
const result = await esClient.indices.getAlias({
|
||||
index: 'pages_alias',
|
||||
})
|
||||
log('result ' + JSON.stringify(result.body))
|
||||
} catch (err) {
|
||||
log('fetch error: ' + err)
|
||||
}
|
||||
|
||||
log('creating newPages')
|
||||
await esClient.indices.create({
|
||||
index: newPages,
|
||||
body: JSON.parse(indexSettings),
|
||||
})
|
||||
|
||||
log('reindexing')
|
||||
await esClient.reindex({
|
||||
body: {
|
||||
source: { index: INDEX_NAME },
|
||||
dest: { index: newPages },
|
||||
},
|
||||
wait_for_completion: true,
|
||||
})
|
||||
|
||||
console.log('updateAliases')
|
||||
|
||||
await esClient.indices.updateAliases({
|
||||
body: {
|
||||
actions: [
|
||||
{
|
||||
add: { index: newPages, alias: 'pages_alias', is_write_index: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
console.log('updateAliases 002')
|
||||
esClient.indices.updateAliases({
|
||||
body: {
|
||||
actions: [{ remove: { index: INDEX_NAME, alias: 'pages_alias' } }],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const updateMappings = async (): Promise<void> => {
|
||||
// update mappings
|
||||
await esClient.indices.putMapping({
|
||||
|
|
@ -131,59 +212,85 @@ const postgresMigration = postgrator
|
|||
process.exit(1)
|
||||
})
|
||||
|
||||
// console.log('reindexing')
|
||||
// reIndex()
|
||||
// .then(() => console.log('reindexed elastic'))
|
||||
// .catch((err) => console.log('error reindexing', err))
|
||||
|
||||
// elastic migration
|
||||
log('Creating elastic index...')
|
||||
const elasticMigration = esClient.indices
|
||||
.exists({ index: INDEX_ALIAS })
|
||||
.then(({ body: exists }) => {
|
||||
if (!exists) {
|
||||
return createIndex().then(() => log('Elastic index created.'))
|
||||
} else {
|
||||
log('Elastic index already exists.')
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
log('Updating elastic index mappings...')
|
||||
return updateMappings().then(() => {
|
||||
log('Elastic index mappings updated.')
|
||||
const indexTest = async (): Promise<void> => {
|
||||
log('running indexTest')
|
||||
try {
|
||||
const result = await esClient.indices.getAlias({
|
||||
index: 'pages_alias',
|
||||
})
|
||||
})
|
||||
.then(() => {
|
||||
log('Adding default state to pages in elastic...')
|
||||
return esClient
|
||||
.update_by_query({
|
||||
index: INDEX_ALIAS,
|
||||
requests_per_second: 250,
|
||||
scroll_size: 500,
|
||||
timeout: '30m',
|
||||
body: {
|
||||
script: {
|
||||
source: 'ctx._source.state = params.state',
|
||||
lang: 'painless',
|
||||
params: {
|
||||
state: 'SUCCEEDED',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
must_not: [
|
||||
{
|
||||
exists: {
|
||||
field: 'state',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(() => log('Default state added.'))
|
||||
})
|
||||
.catch((error) => {
|
||||
log(`${chalk.red('Elastic migration failed: ')}${error.message}`, chalk.red)
|
||||
const { appliedMigrations } = error
|
||||
logAppliedMigrations(appliedMigrations)
|
||||
process.exit(1)
|
||||
})
|
||||
log('result ' + JSON.stringify(result.body))
|
||||
} catch (err) {
|
||||
log('fetch error: ' + err)
|
||||
}
|
||||
}
|
||||
|
||||
Promise.all([postgresMigration, elasticMigration]).then(() => log('Exiting...'))
|
||||
reIndex().then(() => console.log('ran reindex'))
|
||||
// indexTest().then(() => console.log('ran index test'))
|
||||
|
||||
// const elasticMigration = esClient.indices
|
||||
// .exists({ index: INDEX_ALIAS })
|
||||
// .then(({ body: exists }) => {
|
||||
// if (!exists) {
|
||||
// return createIndex().then(() => log('Elastic index created.'))
|
||||
// } else {
|
||||
// log('Elastic index already exists.')
|
||||
// }
|
||||
// })
|
||||
// .then(() => {
|
||||
// log('Updating elastic index mappings...')
|
||||
// return updateMappings().then(() => {
|
||||
// log('Elastic index mappings updated.')
|
||||
// })
|
||||
// })
|
||||
// .then(() => {
|
||||
// log('Adding default state to pages in elastic...')
|
||||
// return esClient
|
||||
// .update_by_query({
|
||||
// index: INDEX_ALIAS,
|
||||
// requests_per_second: 250,
|
||||
// scroll_size: 500,
|
||||
// timeout: '30m',
|
||||
// body: {
|
||||
// script: {
|
||||
// source: 'ctx._source.state = params.state',
|
||||
// lang: 'painless',
|
||||
// params: {
|
||||
// state: 'SUCCEEDED',
|
||||
// },
|
||||
// },
|
||||
// query: {
|
||||
// bool: {
|
||||
// must_not: [
|
||||
// {
|
||||
// exists: {
|
||||
// field: 'state',
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
// .then(() => log('Default state added.'))
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// console.log('error', JSON.stringify(error))
|
||||
// log(`${chalk.red('Elastic migration failed: ')}${error.message}`, chalk.red)
|
||||
// const { appliedMigrations } = error
|
||||
// logAppliedMigrations(appliedMigrations)
|
||||
// process.exit(1)
|
||||
// })
|
||||
|
||||
Promise.all([
|
||||
// postgresMigration,
|
||||
// indexTest,
|
||||
// reIndex,
|
||||
/* elasticMigration */
|
||||
]).then(() => log('Exiting...'))
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export type LibraryItemNode = {
|
|||
siteName?: string
|
||||
subscription?: string
|
||||
readAt?: string
|
||||
recommendedBy?: Recommendation[]
|
||||
recommendations?: Recommendation[]
|
||||
}
|
||||
|
||||
export type Recommendation = {
|
||||
|
|
@ -151,7 +151,7 @@ export function useGetLibraryItemsQuery({
|
|||
siteName
|
||||
subscription
|
||||
readAt
|
||||
recommendedBy {
|
||||
recommendations {
|
||||
id
|
||||
name
|
||||
note
|
||||
|
|
|
|||
Loading…
Reference in a new issue