diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index 3c735a777..e37d5c194 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -11,10 +11,16 @@ on: paths-ignore: - 'apple/**' +env: + NEXT_PUBLIC_APP_ENV: prod + NEXT_PUBLIC_BASE_URL: http://localhost:3000 + NEXT_PUBLIC_SERVER_BASE_URL: http://localhost:4000 + NEXT_PUBLIC_HIGHLIGHTS_BASE_URL: http://localhost:3000 + jobs: run-code-tests: name: Run Codebase tests - runs-on: ubuntu-latest-m + runs-on: ${{ github.repository_owner == 'omnivore-app' && 'ubuntu-latest-m' || 'ubuntu-latest' }} services: postgres: image: ankane/pgvector @@ -59,8 +65,9 @@ jobs: yarn install --frozen-lockfile - name: Database Migration run: | + psql -h localhost -p ${{ job.services.postgres.ports[5432] }} -U postgres -c "CREATE USER app_user WITH ENCRYPTED PASSWORD 'app_pass';" yarn workspace @omnivore/db migrate - psql -h localhost -p ${{ job.services.postgres.ports[5432] }} -U postgres -c "CREATE USER app_user WITH ENCRYPTED PASSWORD 'app_pass';GRANT omnivore_user to app_user;" + psql -h localhost -p ${{ job.services.postgres.ports[5432] }} -U postgres -c "GRANT omnivore_user to app_user;" env: PG_HOST: localhost PG_PORT: ${{ job.services.postgres.ports[5432] }} diff --git a/.gitignore b/.gitignore index ca9c3f95a..96a818d3a 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,5 @@ data.json # android *.aab *.apk + +tsconfig.tsbuildinfo diff --git a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift index de8542a87..e525f5f66 100644 --- a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift +++ b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift @@ -237,13 +237,12 @@ import Utils let pageIndex = Int(event.pageIndex) if let totalPageCount = controller.document?.pageCount { let percent = min(100, max(0, ((Double(pageIndex) + 1.0) / Double(totalPageCount)) * 100.0)) - if percent > self.viewModel.pdfItem.readingProgress { - self.viewModel.updateItemReadProgress( - dataService: dataService, - percent: percent, - anchorIndex: pageIndex - ) - } + self.viewModel.updateItemReadProgress( + dataService: dataService, + percent: percent, + anchorIndex: pageIndex, + force: true + ) } } }.store(in: &subscriptions) diff --git a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift index af5f05915..96f88a654 100644 --- a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift @@ -76,11 +76,12 @@ final class PDFViewerViewModel: ObservableObject { } } - func updateItemReadProgress(dataService: DataService, percent: Double, anchorIndex: Int) { + func updateItemReadProgress(dataService: DataService, percent: Double, anchorIndex: Int, force: Bool = false) { dataService.updateLinkReadingProgress( itemID: pdfItem.itemID, readingProgress: percent, - anchorIndex: anchorIndex + anchorIndex: anchorIndex, + force: force ) } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 498ee5907..c39745961 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -356,34 +356,11 @@ import Views } func markRead(dataService: DataService, item: LinkedItem) { - dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 100, anchorIndex: 0) + dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 100, anchorIndex: 0, force: true) } func markUnread(dataService: DataService, item: LinkedItem) { - dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0, anchorIndex: 0) - } - - func snoozeUntil(dataService: DataService, linkId: String, until: Date, successMessage: String?) async { - isLoading = true - - if let itemIndex = items.firstIndex(where: { $0.id == linkId }) { - items.remove(at: itemIndex) - } - - do { - try await dataService.createReminder( - reminderItemId: .link(id: linkId), - remindAt: until - ) - - if let message = successMessage { - snackbar(message) - } - } catch { - NSNotification.operationFailed(message: "Failed to snooze") - } - - isLoading = false + dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0, anchorIndex: 0, force: true) } private var queryContainsFilter: Bool { @@ -408,12 +385,22 @@ import Views if !selectedLabels.isEmpty { query.append(" label:") - query.append(selectedLabels.map { $0.name != nil ? "\"\(String(describing: $0.name))\"" : "" }.joined(separator: ",")) + query.append(selectedLabels.compactMap { label in + if let name = label.name { + return "\"\(name)\"" + } + return nil + }.joined(separator: ",")) } if !negatedLabels.isEmpty { query.append(" !label:") - query.append(negatedLabels.map { $0.name != nil ? "\"\(String(describing: $0.name))\"" : "" }.joined(separator: ",")) + query.append(negatedLabels.compactMap { label in + if let name = label.name { + return "\"\(name)\"" + } + return nil + }.joined(separator: ",")) } print("QUERY: `\(query)`") diff --git a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift index 1b7e4aa4a..c6149fce0 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift @@ -39,7 +39,8 @@ import Views dataService.updateLinkReadingProgress( itemID: itemID, readingProgress: isItemRead ? 0 : 100, - anchorIndex: 0 + anchorIndex: 0, + force: false ) } diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index f8a5ed8a2..040cc4f7c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -233,7 +233,7 @@ struct WebReaderContainerView: View { ) Button( action: { - dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0, anchorIndex: 0) + dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0, anchorIndex: 0, force: true) }, label: { Label("Reset Read Location", systemImage: "arrow.counterclockwise.circle") } ) @@ -432,7 +432,7 @@ struct WebReaderContainerView: View { #endif .onAppear { if item.isUnread { - dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0.1, anchorIndex: 0) + dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0.1, anchorIndex: 0, force: false) } Task { await audioController.preload(itemIDs: [item.unwrappedID]) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift index 259f10079..2f29e5bc7 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift @@ -171,7 +171,7 @@ struct SafariWebLink: Identifiable { return } - dataService.updateLinkReadingProgress(itemID: itemID, readingProgress: readingProgress, anchorIndex: anchorIndex) + dataService.updateLinkReadingProgress(itemID: itemID, readingProgress: readingProgress, anchorIndex: anchorIndex, force: false) replyHandler(["result": true], nil) } diff --git a/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift b/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift index a0e57e7a7..5fff16478 100644 --- a/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift +++ b/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift @@ -740,7 +740,7 @@ let anchorIndex = Int((player?.currentItem as? SpeechPlayerItem)?.speechItem.htmlIdx ?? "") ?? 0 if let itemID = itemAudioProperties?.itemID { - dataService.updateLinkReadingProgress(itemID: itemID, readingProgress: percentProgress, anchorIndex: anchorIndex) + dataService.updateLinkReadingProgress(itemID: itemID, readingProgress: percentProgress, anchorIndex: anchorIndex, force: true) } if let itemID = itemAudioProperties?.itemID, let player = player, let currentItem = player.currentItem { diff --git a/apple/OmnivoreKit/Sources/Services/AudioSession/Voices.swift b/apple/OmnivoreKit/Sources/Services/AudioSession/Voices.swift index 74df1b51b..fc18983a2 100644 --- a/apple/OmnivoreKit/Sources/Services/AudioSession/Voices.swift +++ b/apple/OmnivoreKit/Sources/Services/AudioSession/Voices.swift @@ -29,6 +29,7 @@ public enum VoiceCategory: String, CaseIterable { case enIN = "English (India)" case enSG = "English (Singapore)" case enUK = "English (UK)" + case frFR = "French (France)" case deDE = "German (Germany)" case hiIN = "Hindi (India)" case itIT = "Italian (Italy)" @@ -70,6 +71,7 @@ public enum Voices { English, VoiceLanguage(key: "zh", name: "Chinese", defaultVoice: "zh-CN-XiaochenNeural", categories: [.zhCN]), VoiceLanguage(key: "de", name: "German", defaultVoice: "de-CH-JanNeural", categories: [.deDE]), + VoiceLanguage(key: "fr", name: "French", defaultVoice: "fr-FR-HenriNeural", categories: [.frFR]), VoiceLanguage(key: "hi", name: "Hindi", defaultVoice: "hi-IN-MadhurNeural", categories: [.hiIN]), VoiceLanguage(key: "it", name: "Italian", defaultVoice: "it-IT-BenignoNeural", categories: [.itIT]), VoiceLanguage(key: "ja", name: "Japanese", defaultVoice: "ja-JP-NanamiNeural", categories: [.jaJP]), @@ -88,6 +90,7 @@ public enum Voices { VoicePair(firstKey: "en-IE-ConnorNeural", secondKey: "en-IE-EmilyNeural", firstName: "Connor", secondName: "Emily", language: "en-IE", category: .enIE), VoicePair(firstKey: "en-IN-NeerjaNeural", secondKey: "en-IN-PrabhatNeural", firstName: "Neerja", secondName: "Prabhat", language: "en-IN", category: .enIN), VoicePair(firstKey: "en-SG-LunaNeural", secondKey: "en-SG-WayneNeural", firstName: "Luna", secondName: "Wayne", language: "en-SG", category: .enSG), + VoicePair(firstKey: "fr-FR-HenriNeural", secondKey: "fr-FR-DeniseNeural", firstName: "Henri", secondName: "Denise", language: "en-FR", category: .frFR), VoicePair(firstKey: "zh-CN-XiaochenNeural", secondKey: "zh-CN-XiaohanNeural", firstName: "Xiaochen", secondName: "Xiaohan", language: "zh-CN", category: .zhCN), VoicePair(firstKey: "zh-CN-XiaoxiaoNeural", secondKey: "zh-CN-YunyangNeural", firstName: "Xiaoxiao", secondName: "Yunyang", language: "zh-CN", category: .zhCN), VoicePair(firstKey: "es-ES-AlvaroNeural", secondKey: "es-ES-ElviraNeural", firstName: "Alvaro", secondName: "Elvira", language: "es-ES", category: .esES), diff --git a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift index fb6283215..32e46f8f0 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift @@ -1515,7 +1515,7 @@ extension Fields where TypeLock == Objects.Article { } } - func updatedAt() throws -> DateTime { + func updatedAt() throws -> DateTime? { let field = GraphQLField.leaf( name: "updatedAt", arguments: [] @@ -1524,12 +1524,9 @@ extension Fields where TypeLock == Objects.Article { switch response { case let .decoding(data): - if let data = data.updatedAt[field.alias!] { - return data - } - throw HttpError.badpayload + return data.updatedAt[field.alias!] case .mocking: - return DateTime.mockValue + return nil } } @@ -1941,7 +1938,7 @@ extension Fields where TypeLock == Objects.ArticleSavingRequest { } } - func updatedAt() throws -> DateTime { + func updatedAt() throws -> DateTime? { let field = GraphQLField.leaf( name: "updatedAt", arguments: [] @@ -1950,12 +1947,9 @@ extension Fields where TypeLock == Objects.ArticleSavingRequest { switch response { case let .decoding(data): - if let data = data.updatedAt[field.alias!] { - return data - } - throw HttpError.badpayload + return data.updatedAt[field.alias!] case .mocking: - return DateTime.mockValue + return nil } } @@ -5595,7 +5589,7 @@ extension Fields where TypeLock == Objects.Feature { } } - func updatedAt() throws -> DateTime { + func updatedAt() throws -> DateTime? { let field = GraphQLField.leaf( name: "updatedAt", arguments: [] @@ -5604,12 +5598,9 @@ extension Fields where TypeLock == Objects.Feature { switch response { case let .decoding(data): - if let data = data.updatedAt[field.alias!] { - return data - } - throw HttpError.badpayload + return data.updatedAt[field.alias!] case .mocking: - return DateTime.mockValue + return nil } } } @@ -6139,12 +6130,14 @@ extension Objects { let __typename: TypeName = .filter let category: [String: String] let createdAt: [String: DateTime] + let defaultFilter: [String: Bool] let description: [String: String] let filter: [String: String] let id: [String: String] let name: [String: String] let position: [String: Int] let updatedAt: [String: DateTime] + let visible: [String: Bool] enum TypeName: String, Codable { case filter = "Filter" @@ -6172,6 +6165,10 @@ extension Objects.Filter: Decodable { if let value = try container.decode(DateTime?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "defaultFilter": + if let value = try container.decode(Bool?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "description": if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -6196,6 +6193,10 @@ extension Objects.Filter: Decodable { if let value = try container.decode(DateTime?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "visible": + if let value = try container.decode(Bool?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } default: throw DecodingError.dataCorrupted( DecodingError.Context( @@ -6208,12 +6209,14 @@ extension Objects.Filter: Decodable { category = map["category"] createdAt = map["createdAt"] + defaultFilter = map["defaultFilter"] description = map["description"] filter = map["filter"] id = map["id"] name = map["name"] position = map["position"] updatedAt = map["updatedAt"] + visible = map["visible"] } } @@ -6254,6 +6257,21 @@ extension Fields where TypeLock == Objects.Filter { } } + func defaultFilter() throws -> Bool? { + let field = GraphQLField.leaf( + name: "defaultFilter", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.defaultFilter[field.alias!] + case .mocking: + return nil + } + } + func description() throws -> String? { let field = GraphQLField.leaf( name: "description", @@ -6341,7 +6359,7 @@ extension Fields where TypeLock == Objects.Filter { } } - func updatedAt() throws -> DateTime { + func updatedAt() throws -> DateTime? { let field = GraphQLField.leaf( name: "updatedAt", arguments: [] @@ -6350,12 +6368,24 @@ extension Fields where TypeLock == Objects.Filter { switch response { case let .decoding(data): - if let data = data.updatedAt[field.alias!] { - return data - } - throw HttpError.badpayload + return data.updatedAt[field.alias!] case .mocking: - return DateTime.mockValue + return nil + } + } + + func visible() throws -> Bool? { + let field = GraphQLField.leaf( + name: "visible", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.visible[field.alias!] + case .mocking: + return nil } } } @@ -7728,7 +7758,7 @@ extension Fields where TypeLock == Objects.Highlight { } } - func updatedAt() throws -> DateTime { + func updatedAt() throws -> DateTime? { let field = GraphQLField.leaf( name: "updatedAt", arguments: [] @@ -7737,12 +7767,9 @@ extension Fields where TypeLock == Objects.Highlight { switch response { case let .decoding(data): - if let data = data.updatedAt[field.alias!] { - return data - } - throw HttpError.badpayload + return data.updatedAt[field.alias!] case .mocking: - return DateTime.mockValue + return nil } } @@ -7915,7 +7942,7 @@ extension Fields where TypeLock == Objects.HighlightReply { } } - func updatedAt() throws -> DateTime { + func updatedAt() throws -> DateTime? { let field = GraphQLField.leaf( name: "updatedAt", arguments: [] @@ -7924,12 +7951,9 @@ extension Fields where TypeLock == Objects.HighlightReply { switch response { case let .decoding(data): - if let data = data.updatedAt[field.alias!] { - return data - } - throw HttpError.badpayload + return data.updatedAt[field.alias!] case .mocking: - return DateTime.mockValue + return nil } } @@ -8359,7 +8383,7 @@ extension Fields where TypeLock == Objects.Integration { } } - func updatedAt() throws -> DateTime { + func updatedAt() throws -> DateTime? { let field = GraphQLField.leaf( name: "updatedAt", arguments: [] @@ -8368,12 +8392,9 @@ extension Fields where TypeLock == Objects.Integration { switch response { case let .decoding(data): - if let data = data.updatedAt[field.alias!] { - return data - } - throw HttpError.badpayload + return data.updatedAt[field.alias!] case .mocking: - return DateTime.mockValue + return nil } } } @@ -9420,7 +9441,7 @@ extension Fields where TypeLock == Objects.Link { } } - func updatedAt() throws -> DateTime { + func updatedAt() throws -> DateTime? { let field = GraphQLField.leaf( name: "updatedAt", arguments: [] @@ -9429,12 +9450,9 @@ extension Fields where TypeLock == Objects.Link { switch response { case let .decoding(data): - if let data = data.updatedAt[field.alias!] { - return data - } - throw HttpError.badpayload + return data.updatedAt[field.alias!] case .mocking: - return DateTime.mockValue + return nil } } @@ -10388,20 +10406,14 @@ extension Objects { let createArticleSavingRequest: [String: Unions.CreateArticleSavingRequestResult] let createGroup: [String: Unions.CreateGroupResult] let createHighlight: [String: Unions.CreateHighlightResult] - let createHighlightReply: [String: Unions.CreateHighlightReplyResult] let createLabel: [String: Unions.CreateLabelResult] let createNewsletterEmail: [String: Unions.CreateNewsletterEmailResult] - let createReaction: [String: Unions.CreateReactionResult] - let createReminder: [String: Unions.CreateReminderResult] let deleteAccount: [String: Unions.DeleteAccountResult] let deleteFilter: [String: Unions.DeleteFilterResult] let deleteHighlight: [String: Unions.DeleteHighlightResult] - let deleteHighlightReply: [String: Unions.DeleteHighlightReplyResult] let deleteIntegration: [String: Unions.DeleteIntegrationResult] let deleteLabel: [String: Unions.DeleteLabelResult] let deleteNewsletterEmail: [String: Unions.DeleteNewsletterEmailResult] - let deleteReaction: [String: Unions.DeleteReactionResult] - let deleteReminder: [String: Unions.DeleteReminderResult] let deleteRule: [String: Unions.DeleteRuleResult] let deleteWebhook: [String: Unions.DeleteWebhookResult] let generateApiKey: [String: Unions.GenerateApiKeyResult] @@ -10428,25 +10440,20 @@ extension Objects { let setBookmarkArticle: [String: Unions.SetBookmarkArticleResult] let setDeviceToken: [String: Unions.SetDeviceTokenResult] let setFavoriteArticle: [String: Unions.SetFavoriteArticleResult] - let setFollow: [String: Unions.SetFollowResult] let setIntegration: [String: Unions.SetIntegrationResult] let setLabels: [String: Unions.SetLabelsResult] let setLabelsForHighlight: [String: Unions.SetLabelsResult] let setLinkArchived: [String: Unions.ArchiveLinkResult] let setRule: [String: Unions.SetRuleResult] - let setShareArticle: [String: Unions.SetShareArticleResult] - let setShareHighlight: [String: Unions.SetShareHighlightResult] let setUserPersonalization: [String: Unions.SetUserPersonalizationResult] let setWebhook: [String: Unions.SetWebhookResult] let subscribe: [String: Unions.SubscribeResult] let unsubscribe: [String: Unions.UnsubscribeResult] + let updateEmail: [String: Unions.UpdateEmailResult] + let updateFilter: [String: Unions.UpdateFilterResult] let updateHighlight: [String: Unions.UpdateHighlightResult] - let updateHighlightReply: [String: Unions.UpdateHighlightReplyResult] let updateLabel: [String: Unions.UpdateLabelResult] - let updateLinkShareInfo: [String: Unions.UpdateLinkShareInfoResult] let updatePage: [String: Unions.UpdatePageResult] - let updateReminder: [String: Unions.UpdateReminderResult] - let updateSharedComment: [String: Unions.UpdateSharedCommentResult] let updateSubscription: [String: Unions.UpdateSubscriptionResult] let updateUser: [String: Unions.UpdateUserResult] let updateUserProfile: [String: Unions.UpdateUserProfileResult] @@ -10495,10 +10502,6 @@ extension Objects.Mutation: Decodable { if let value = try container.decode(Unions.CreateHighlightResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } - case "createHighlightReply": - if let value = try container.decode(Unions.CreateHighlightReplyResult?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } case "createLabel": if let value = try container.decode(Unions.CreateLabelResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -10507,14 +10510,6 @@ extension Objects.Mutation: Decodable { if let value = try container.decode(Unions.CreateNewsletterEmailResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } - case "createReaction": - if let value = try container.decode(Unions.CreateReactionResult?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } - case "createReminder": - if let value = try container.decode(Unions.CreateReminderResult?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } case "deleteAccount": if let value = try container.decode(Unions.DeleteAccountResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -10527,10 +10522,6 @@ extension Objects.Mutation: Decodable { if let value = try container.decode(Unions.DeleteHighlightResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } - case "deleteHighlightReply": - if let value = try container.decode(Unions.DeleteHighlightReplyResult?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } case "deleteIntegration": if let value = try container.decode(Unions.DeleteIntegrationResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -10543,14 +10534,6 @@ extension Objects.Mutation: Decodable { if let value = try container.decode(Unions.DeleteNewsletterEmailResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } - case "deleteReaction": - if let value = try container.decode(Unions.DeleteReactionResult?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } - case "deleteReminder": - if let value = try container.decode(Unions.DeleteReminderResult?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } case "deleteRule": if let value = try container.decode(Unions.DeleteRuleResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -10655,10 +10638,6 @@ extension Objects.Mutation: Decodable { if let value = try container.decode(Unions.SetFavoriteArticleResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } - case "setFollow": - if let value = try container.decode(Unions.SetFollowResult?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } case "setIntegration": if let value = try container.decode(Unions.SetIntegrationResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -10679,14 +10658,6 @@ extension Objects.Mutation: Decodable { if let value = try container.decode(Unions.SetRuleResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } - case "setShareArticle": - if let value = try container.decode(Unions.SetShareArticleResult?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } - case "setShareHighlight": - if let value = try container.decode(Unions.SetShareHighlightResult?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } case "setUserPersonalization": if let value = try container.decode(Unions.SetUserPersonalizationResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -10703,34 +10674,26 @@ extension Objects.Mutation: Decodable { if let value = try container.decode(Unions.UnsubscribeResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } - case "updateHighlight": - if let value = try container.decode(Unions.UpdateHighlightResult?.self, forKey: codingKey) { + case "updateEmail": + if let value = try container.decode(Unions.UpdateEmailResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } - case "updateHighlightReply": - if let value = try container.decode(Unions.UpdateHighlightReplyResult?.self, forKey: codingKey) { + case "updateFilter": + if let value = try container.decode(Unions.UpdateFilterResult?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "updateHighlight": + if let value = try container.decode(Unions.UpdateHighlightResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } case "updateLabel": if let value = try container.decode(Unions.UpdateLabelResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } - case "updateLinkShareInfo": - if let value = try container.decode(Unions.UpdateLinkShareInfoResult?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } case "updatePage": if let value = try container.decode(Unions.UpdatePageResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } - case "updateReminder": - if let value = try container.decode(Unions.UpdateReminderResult?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } - case "updateSharedComment": - if let value = try container.decode(Unions.UpdateSharedCommentResult?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } case "updateSubscription": if let value = try container.decode(Unions.UpdateSubscriptionResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -10767,20 +10730,14 @@ extension Objects.Mutation: Decodable { createArticleSavingRequest = map["createArticleSavingRequest"] createGroup = map["createGroup"] createHighlight = map["createHighlight"] - createHighlightReply = map["createHighlightReply"] createLabel = map["createLabel"] createNewsletterEmail = map["createNewsletterEmail"] - createReaction = map["createReaction"] - createReminder = map["createReminder"] deleteAccount = map["deleteAccount"] deleteFilter = map["deleteFilter"] deleteHighlight = map["deleteHighlight"] - deleteHighlightReply = map["deleteHighlightReply"] deleteIntegration = map["deleteIntegration"] deleteLabel = map["deleteLabel"] deleteNewsletterEmail = map["deleteNewsletterEmail"] - deleteReaction = map["deleteReaction"] - deleteReminder = map["deleteReminder"] deleteRule = map["deleteRule"] deleteWebhook = map["deleteWebhook"] generateApiKey = map["generateApiKey"] @@ -10807,25 +10764,20 @@ extension Objects.Mutation: Decodable { setBookmarkArticle = map["setBookmarkArticle"] setDeviceToken = map["setDeviceToken"] setFavoriteArticle = map["setFavoriteArticle"] - setFollow = map["setFollow"] setIntegration = map["setIntegration"] setLabels = map["setLabels"] setLabelsForHighlight = map["setLabelsForHighlight"] setLinkArchived = map["setLinkArchived"] setRule = map["setRule"] - setShareArticle = map["setShareArticle"] - setShareHighlight = map["setShareHighlight"] setUserPersonalization = map["setUserPersonalization"] setWebhook = map["setWebhook"] subscribe = map["subscribe"] unsubscribe = map["unsubscribe"] + updateEmail = map["updateEmail"] + updateFilter = map["updateFilter"] updateHighlight = map["updateHighlight"] - updateHighlightReply = map["updateHighlightReply"] updateLabel = map["updateLabel"] - updateLinkShareInfo = map["updateLinkShareInfo"] updatePage = map["updatePage"] - updateReminder = map["updateReminder"] - updateSharedComment = map["updateSharedComment"] updateSubscription = map["updateSubscription"] updateUser = map["updateUser"] updateUserProfile = map["updateUserProfile"] @@ -10949,25 +10901,6 @@ extension Fields where TypeLock == Objects.Mutation { } } - func createHighlightReply(input: InputObjects.CreateHighlightReplyInput, selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "createHighlightReply", - arguments: [Argument(name: "input", type: "CreateHighlightReplyInput!", value: input)], - selection: selection.selection - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.createHighlightReply[field.alias!] { - return try selection.decode(data: data) - } - throw HttpError.badpayload - case .mocking: - return selection.mock() - } - } - func createLabel(input: InputObjects.CreateLabelInput, selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "createLabel", @@ -11006,44 +10939,6 @@ extension Fields where TypeLock == Objects.Mutation { } } - func createReaction(input: InputObjects.CreateReactionInput, selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "createReaction", - arguments: [Argument(name: "input", type: "CreateReactionInput!", value: input)], - selection: selection.selection - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.createReaction[field.alias!] { - return try selection.decode(data: data) - } - throw HttpError.badpayload - case .mocking: - return selection.mock() - } - } - - func createReminder(input: InputObjects.CreateReminderInput, selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "createReminder", - arguments: [Argument(name: "input", type: "CreateReminderInput!", value: input)], - selection: selection.selection - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.createReminder[field.alias!] { - return try selection.decode(data: data) - } - throw HttpError.badpayload - case .mocking: - return selection.mock() - } - } - func deleteAccount(userId: String, selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "deleteAccount", @@ -11101,25 +10996,6 @@ extension Fields where TypeLock == Objects.Mutation { } } - func deleteHighlightReply(highlightReplyId: String, selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "deleteHighlightReply", - arguments: [Argument(name: "highlightReplyId", type: "ID!", value: highlightReplyId)], - selection: selection.selection - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.deleteHighlightReply[field.alias!] { - return try selection.decode(data: data) - } - throw HttpError.badpayload - case .mocking: - return selection.mock() - } - } - func deleteIntegration(id: String, selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "deleteIntegration", @@ -11177,44 +11053,6 @@ extension Fields where TypeLock == Objects.Mutation { } } - func deleteReaction(id: String, selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "deleteReaction", - arguments: [Argument(name: "id", type: "ID!", value: id)], - selection: selection.selection - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.deleteReaction[field.alias!] { - return try selection.decode(data: data) - } - throw HttpError.badpayload - case .mocking: - return selection.mock() - } - } - - func deleteReminder(id: String, selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "deleteReminder", - arguments: [Argument(name: "id", type: "ID!", value: id)], - selection: selection.selection - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.deleteReminder[field.alias!] { - return try selection.decode(data: data) - } - throw HttpError.badpayload - case .mocking: - return selection.mock() - } - } - func deleteRule(id: String, selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "deleteRule", @@ -11709,25 +11547,6 @@ extension Fields where TypeLock == Objects.Mutation { } } - func setFollow(input: InputObjects.SetFollowInput, selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "setFollow", - arguments: [Argument(name: "input", type: "SetFollowInput!", value: input)], - selection: selection.selection - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.setFollow[field.alias!] { - return try selection.decode(data: data) - } - throw HttpError.badpayload - case .mocking: - return selection.mock() - } - } - func setIntegration(input: InputObjects.SetIntegrationInput, selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "setIntegration", @@ -11823,44 +11642,6 @@ extension Fields where TypeLock == Objects.Mutation { } } - func setShareArticle(input: InputObjects.SetShareArticleInput, selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "setShareArticle", - arguments: [Argument(name: "input", type: "SetShareArticleInput!", value: input)], - selection: selection.selection - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.setShareArticle[field.alias!] { - return try selection.decode(data: data) - } - throw HttpError.badpayload - case .mocking: - return selection.mock() - } - } - - func setShareHighlight(input: InputObjects.SetShareHighlightInput, selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "setShareHighlight", - arguments: [Argument(name: "input", type: "SetShareHighlightInput!", value: input)], - selection: selection.selection - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.setShareHighlight[field.alias!] { - return try selection.decode(data: data) - } - throw HttpError.badpayload - case .mocking: - return selection.mock() - } - } - func setUserPersonalization(input: InputObjects.SetUserPersonalizationInput, selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "setUserPersonalization", @@ -11937,6 +11718,44 @@ extension Fields where TypeLock == Objects.Mutation { } } + func updateEmail(input: InputObjects.UpdateEmailInput, selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "updateEmail", + arguments: [Argument(name: "input", type: "UpdateEmailInput!", value: input)], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.updateEmail[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } + + func updateFilter(input: InputObjects.UpdateFilterInput, selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "updateFilter", + arguments: [Argument(name: "input", type: "UpdateFilterInput!", value: input)], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.updateFilter[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } + func updateHighlight(input: InputObjects.UpdateHighlightInput, selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "updateHighlight", @@ -11956,25 +11775,6 @@ extension Fields where TypeLock == Objects.Mutation { } } - func updateHighlightReply(input: InputObjects.UpdateHighlightReplyInput, selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "updateHighlightReply", - arguments: [Argument(name: "input", type: "UpdateHighlightReplyInput!", value: input)], - selection: selection.selection - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.updateHighlightReply[field.alias!] { - return try selection.decode(data: data) - } - throw HttpError.badpayload - case .mocking: - return selection.mock() - } - } - func updateLabel(input: InputObjects.UpdateLabelInput, selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "updateLabel", @@ -11994,25 +11794,6 @@ extension Fields where TypeLock == Objects.Mutation { } } - func updateLinkShareInfo(input: InputObjects.UpdateLinkShareInfoInput, selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "updateLinkShareInfo", - arguments: [Argument(name: "input", type: "UpdateLinkShareInfoInput!", value: input)], - selection: selection.selection - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.updateLinkShareInfo[field.alias!] { - return try selection.decode(data: data) - } - throw HttpError.badpayload - case .mocking: - return selection.mock() - } - } - func updatePage(input: InputObjects.UpdatePageInput, selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "updatePage", @@ -12032,44 +11813,6 @@ extension Fields where TypeLock == Objects.Mutation { } } - func updateReminder(input: InputObjects.UpdateReminderInput, selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "updateReminder", - arguments: [Argument(name: "input", type: "UpdateReminderInput!", value: input)], - selection: selection.selection - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.updateReminder[field.alias!] { - return try selection.decode(data: data) - } - throw HttpError.badpayload - case .mocking: - return selection.mock() - } - } - - func updateSharedComment(input: InputObjects.UpdateSharedCommentInput, selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "updateSharedComment", - arguments: [Argument(name: "input", type: "UpdateSharedCommentInput!", value: input)], - selection: selection.selection - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.updateSharedComment[field.alias!] { - return try selection.decode(data: data) - } - throw HttpError.badpayload - case .mocking: - return selection.mock() - } - } - func updateSubscription(input: InputObjects.UpdateSubscriptionInput, selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "updateSubscription", @@ -12917,7 +12660,7 @@ extension Fields where TypeLock == Objects.Page { } } - func updatedAt() throws -> DateTime { + func updatedAt() throws -> DateTime? { let field = GraphQLField.leaf( name: "updatedAt", arguments: [] @@ -12926,12 +12669,9 @@ extension Fields where TypeLock == Objects.Page { switch response { case let .decoding(data): - if let data = data.updatedAt[field.alias!] { - return data - } - throw HttpError.badpayload + return data.updatedAt[field.alias!] case .mocking: - return DateTime.mockValue + return nil } } @@ -13271,12 +13011,8 @@ extension Objects { let apiKeys: [String: Unions.ApiKeysResult] let article: [String: Unions.ArticleResult] let articleSavingRequest: [String: Unions.ArticleSavingRequestResult] - let articles: [String: Unions.ArticlesResult] let deviceTokens: [String: Unions.DeviceTokensResult] - let feedArticles: [String: Unions.FeedArticlesResult] let filters: [String: Unions.FiltersResult] - let getFollowers: [String: Unions.GetFollowersResult] - let getFollowing: [String: Unions.GetFollowingResult] let getUserPersonalization: [String: Unions.GetUserPersonalizationResult] let groups: [String: Unions.GroupsResult] let hello: [String: String] @@ -13286,11 +13022,9 @@ extension Objects { let newsletterEmails: [String: Unions.NewsletterEmailsResult] let recentEmails: [String: Unions.RecentEmailsResult] let recentSearches: [String: Unions.RecentSearchesResult] - let reminder: [String: Unions.ReminderResult] let rules: [String: Unions.RulesResult] let search: [String: Unions.SearchResult] let sendInstallInstructions: [String: Unions.SendInstallInstructionsResult] - let sharedArticle: [String: Unions.SharedArticleResult] let subscriptions: [String: Unions.SubscriptionsResult] let typeaheadSearch: [String: Unions.TypeaheadSearchResult] let updatesSince: [String: Unions.UpdatesSinceResult] @@ -13330,30 +13064,14 @@ extension Objects.Query: Decodable { if let value = try container.decode(Unions.ArticleSavingRequestResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } - case "articles": - if let value = try container.decode(Unions.ArticlesResult?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } case "deviceTokens": if let value = try container.decode(Unions.DeviceTokensResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } - case "feedArticles": - if let value = try container.decode(Unions.FeedArticlesResult?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } case "filters": if let value = try container.decode(Unions.FiltersResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } - case "getFollowers": - if let value = try container.decode(Unions.GetFollowersResult?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } - case "getFollowing": - if let value = try container.decode(Unions.GetFollowingResult?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } case "getUserPersonalization": if let value = try container.decode(Unions.GetUserPersonalizationResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -13390,10 +13108,6 @@ extension Objects.Query: Decodable { if let value = try container.decode(Unions.RecentSearchesResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } - case "reminder": - if let value = try container.decode(Unions.ReminderResult?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } case "rules": if let value = try container.decode(Unions.RulesResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -13406,10 +13120,6 @@ extension Objects.Query: Decodable { if let value = try container.decode(Unions.SendInstallInstructionsResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } - case "sharedArticle": - if let value = try container.decode(Unions.SharedArticleResult?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } case "subscriptions": if let value = try container.decode(Unions.SubscriptionsResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -13455,12 +13165,8 @@ extension Objects.Query: Decodable { apiKeys = map["apiKeys"] article = map["article"] articleSavingRequest = map["articleSavingRequest"] - articles = map["articles"] deviceTokens = map["deviceTokens"] - feedArticles = map["feedArticles"] filters = map["filters"] - getFollowers = map["getFollowers"] - getFollowing = map["getFollowing"] getUserPersonalization = map["getUserPersonalization"] groups = map["groups"] hello = map["hello"] @@ -13470,11 +13176,9 @@ extension Objects.Query: Decodable { newsletterEmails = map["newsletterEmails"] recentEmails = map["recentEmails"] recentSearches = map["recentSearches"] - reminder = map["reminder"] rules = map["rules"] search = map["search"] sendInstallInstructions = map["sendInstallInstructions"] - sharedArticle = map["sharedArticle"] subscriptions = map["subscriptions"] typeaheadSearch = map["typeaheadSearch"] updatesSince = map["updatesSince"] @@ -13544,25 +13248,6 @@ extension Fields where TypeLock == Objects.Query { } } - func articles(after: OptionalArgument = .absent(), first: OptionalArgument = .absent(), includePending: OptionalArgument = .absent(), query: OptionalArgument = .absent(), sharedOnly: OptionalArgument = .absent(), sort: OptionalArgument = .absent(), selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "articles", - arguments: [Argument(name: "after", type: "String", value: after), Argument(name: "first", type: "Int", value: first), Argument(name: "includePending", type: "Boolean", value: includePending), Argument(name: "query", type: "String", value: query), Argument(name: "sharedOnly", type: "Boolean", value: sharedOnly), Argument(name: "sort", type: "SortParams", value: sort)], - selection: selection.selection - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.articles[field.alias!] { - return try selection.decode(data: data) - } - throw HttpError.badpayload - case .mocking: - return selection.mock() - } - } - func deviceTokens(selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "deviceTokens", @@ -13582,25 +13267,6 @@ extension Fields where TypeLock == Objects.Query { } } - func feedArticles(after: OptionalArgument = .absent(), first: OptionalArgument = .absent(), sharedByUser: OptionalArgument = .absent(), sort: OptionalArgument = .absent(), selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "feedArticles", - arguments: [Argument(name: "after", type: "String", value: after), Argument(name: "first", type: "Int", value: first), Argument(name: "sharedByUser", type: "ID", value: sharedByUser), Argument(name: "sort", type: "SortParams", value: sort)], - selection: selection.selection - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.feedArticles[field.alias!] { - return try selection.decode(data: data) - } - throw HttpError.badpayload - case .mocking: - return selection.mock() - } - } - func filters(selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "filters", @@ -13620,44 +13286,6 @@ extension Fields where TypeLock == Objects.Query { } } - func getFollowers(userId: OptionalArgument = .absent(), selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "getFollowers", - arguments: [Argument(name: "userId", type: "ID", value: userId)], - selection: selection.selection - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.getFollowers[field.alias!] { - return try selection.decode(data: data) - } - throw HttpError.badpayload - case .mocking: - return selection.mock() - } - } - - func getFollowing(userId: OptionalArgument = .absent(), selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "getFollowing", - arguments: [Argument(name: "userId", type: "ID", value: userId)], - selection: selection.selection - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.getFollowing[field.alias!] { - return try selection.decode(data: data) - } - throw HttpError.badpayload - case .mocking: - return selection.mock() - } - } - func getUserPersonalization(selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "getUserPersonalization", @@ -13822,25 +13450,6 @@ extension Fields where TypeLock == Objects.Query { } } - func reminder(linkId: String, selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "reminder", - arguments: [Argument(name: "linkId", type: "ID!", value: linkId)], - selection: selection.selection - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.reminder[field.alias!] { - return try selection.decode(data: data) - } - throw HttpError.badpayload - case .mocking: - return selection.mock() - } - } - func rules(enabled: OptionalArgument = .absent(), selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "rules", @@ -13898,25 +13507,6 @@ extension Fields where TypeLock == Objects.Query { } } - func sharedArticle(selectedHighlightId: OptionalArgument = .absent(), slug: String, username: String, selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "sharedArticle", - arguments: [Argument(name: "selectedHighlightId", type: "String", value: selectedHighlightId), Argument(name: "slug", type: "String!", value: slug), Argument(name: "username", type: "String!", value: username)], - selection: selection.selection - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.sharedArticle[field.alias!] { - return try selection.decode(data: data) - } - throw HttpError.badpayload - case .mocking: - return selection.mock() - } - } - func subscriptions(sort: OptionalArgument = .absent(), type: OptionalArgument = .absent(), selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "subscriptions", @@ -15662,7 +15252,7 @@ extension Fields where TypeLock == Objects.RecommendationGroup { } } - func updatedAt() throws -> DateTime { + func updatedAt() throws -> DateTime? { let field = GraphQLField.leaf( name: "updatedAt", arguments: [] @@ -15671,12 +15261,9 @@ extension Fields where TypeLock == Objects.RecommendationGroup { switch response { case let .decoding(data): - if let data = data.updatedAt[field.alias!] { - return data - } - throw HttpError.badpayload + return data.updatedAt[field.alias!] case .mocking: - return DateTime.mockValue + return nil } } } @@ -16494,7 +16081,7 @@ extension Fields where TypeLock == Objects.Rule { } } - func updatedAt() throws -> DateTime { + func updatedAt() throws -> DateTime? { let field = GraphQLField.leaf( name: "updatedAt", arguments: [] @@ -16503,12 +16090,9 @@ extension Fields where TypeLock == Objects.Rule { switch response { case let .decoding(data): - if let data = data.updatedAt[field.alias!] { - return data - } - throw HttpError.badpayload + return data.updatedAt[field.alias!] case .mocking: - return DateTime.mockValue + return nil } } } @@ -18776,7 +18360,7 @@ extension Selection where TypeLock == Never, Type == Never { extension Objects { struct SetFavoriteArticleSuccess { let __typename: TypeName = .setFavoriteArticleSuccess - let favoriteArticle: [String: Objects.Article] + let success: [String: Bool] enum TypeName: String, Codable { case setFavoriteArticleSuccess = "SetFavoriteArticleSuccess" @@ -18796,8 +18380,8 @@ extension Objects.SetFavoriteArticleSuccess: Decodable { let field = GraphQLField.getFieldNameFromAlias(alias) switch field { - case "favoriteArticle": - if let value = try container.decode(Objects.Article?.self, forKey: codingKey) { + case "success": + if let value = try container.decode(Bool?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } default: @@ -18810,27 +18394,26 @@ extension Objects.SetFavoriteArticleSuccess: Decodable { } } - favoriteArticle = map["favoriteArticle"] + success = map["success"] } } extension Fields where TypeLock == Objects.SetFavoriteArticleSuccess { - func favoriteArticle(selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "favoriteArticle", - arguments: [], - selection: selection.selection + func success() throws -> Bool { + let field = GraphQLField.leaf( + name: "success", + arguments: [] ) select(field) switch response { case let .decoding(data): - if let data = data.favoriteArticle[field.alias!] { - return try selection.decode(data: data) + if let data = data.success[field.alias!] { + return data } throw HttpError.badpayload case .mocking: - return selection.mock() + return Bool.mockValue } } } @@ -20623,7 +20206,7 @@ extension Fields where TypeLock == Objects.Subscription { } } - func updatedAt() throws -> DateTime { + func updatedAt() throws -> DateTime? { let field = GraphQLField.leaf( name: "updatedAt", arguments: [] @@ -20632,12 +20215,9 @@ extension Fields where TypeLock == Objects.Subscription { switch response { case let .decoding(data): - if let data = data.updatedAt[field.alias!] { - return data - } - throw HttpError.badpayload + return data.updatedAt[field.alias!] case .mocking: - return DateTime.mockValue + return nil } } @@ -21347,6 +20927,288 @@ extension Selection where TypeLock == Never, Type == Never { typealias UnsubscribeSuccess = Selection } +extension Objects { + struct UpdateEmailError { + let __typename: TypeName = .updateEmailError + let errorCodes: [String: [Enums.UpdateEmailErrorCode]] + + enum TypeName: String, Codable { + case updateEmailError = "UpdateEmailError" + } + } +} + +extension Objects.UpdateEmailError: 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.UpdateEmailErrorCode]?.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.UpdateEmailError { + func errorCodes() throws -> [Enums.UpdateEmailErrorCode] { + 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 UpdateEmailError = Selection +} + +extension Objects { + struct UpdateEmailSuccess { + let __typename: TypeName = .updateEmailSuccess + let email: [String: String] + let verificationEmailSent: [String: Bool] + + enum TypeName: String, Codable { + case updateEmailSuccess = "UpdateEmailSuccess" + } + } +} + +extension Objects.UpdateEmailSuccess: 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 "email": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "verificationEmailSent": + if let value = try container.decode(Bool?.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)." + ) + ) + } + } + + email = map["email"] + verificationEmailSent = map["verificationEmailSent"] + } +} + +extension Fields where TypeLock == Objects.UpdateEmailSuccess { + func email() throws -> String { + let field = GraphQLField.leaf( + name: "email", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.email[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return String.mockValue + } + } + + func verificationEmailSent() throws -> Bool? { + let field = GraphQLField.leaf( + name: "verificationEmailSent", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.verificationEmailSent[field.alias!] + case .mocking: + return nil + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias UpdateEmailSuccess = Selection +} + +extension Objects { + struct UpdateFilterError { + let __typename: TypeName = .updateFilterError + let errorCodes: [String: [Enums.UpdateFilterErrorCode]] + + enum TypeName: String, Codable { + case updateFilterError = "UpdateFilterError" + } + } +} + +extension Objects.UpdateFilterError: 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.UpdateFilterErrorCode]?.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.UpdateFilterError { + func errorCodes() throws -> [Enums.UpdateFilterErrorCode] { + 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 UpdateFilterError = Selection +} + +extension Objects { + struct UpdateFilterSuccess { + let __typename: TypeName = .updateFilterSuccess + let filter: [String: Objects.Filter] + + enum TypeName: String, Codable { + case updateFilterSuccess = "UpdateFilterSuccess" + } + } +} + +extension Objects.UpdateFilterSuccess: 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 "filter": + if let value = try container.decode(Objects.Filter?.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)." + ) + ) + } + } + + filter = map["filter"] + } +} + +extension Fields where TypeLock == Objects.UpdateFilterSuccess { + func filter(selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "filter", + arguments: [], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.filter[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias UpdateFilterSuccess = Selection +} + extension Objects { struct UpdateHighlightError { let __typename: TypeName = .updateHighlightError @@ -23158,6 +23020,7 @@ extension Selection where TypeLock == Never, Type == Never { extension Objects { struct User { let __typename: TypeName = .user + let email: [String: String] let followersCount: [String: Int] let friendsCount: [String: Int] let id: [String: String] @@ -23170,6 +23033,7 @@ extension Objects { let sharedArticlesCount: [String: Int] let sharedHighlightsCount: [String: Int] let sharedNotesCount: [String: Int] + let source: [String: String] let viewerIsFollowing: [String: Bool] enum TypeName: String, Codable { @@ -23190,6 +23054,10 @@ extension Objects.User: Decodable { let field = GraphQLField.getFieldNameFromAlias(alias) switch field { + case "email": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "followersCount": if let value = try container.decode(Int?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -23238,6 +23106,10 @@ extension Objects.User: Decodable { if let value = try container.decode(Int?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "source": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "viewerIsFollowing": if let value = try container.decode(Bool?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -23252,6 +23124,7 @@ extension Objects.User: Decodable { } } + email = map["email"] followersCount = map["followersCount"] friendsCount = map["friendsCount"] id = map["id"] @@ -23264,11 +23137,27 @@ extension Objects.User: Decodable { sharedArticlesCount = map["sharedArticlesCount"] sharedHighlightsCount = map["sharedHighlightsCount"] sharedNotesCount = map["sharedNotesCount"] + source = map["source"] viewerIsFollowing = map["viewerIsFollowing"] } } extension Fields where TypeLock == Objects.User { + func email() throws -> String? { + let field = GraphQLField.leaf( + name: "email", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.email[field.alias!] + case .mocking: + return nil + } + } + func followersCount() throws -> Int? { let field = GraphQLField.leaf( name: "followersCount", @@ -23464,6 +23353,21 @@ extension Fields where TypeLock == Objects.User { } } + func source() throws -> String? { + let field = GraphQLField.leaf( + name: "source", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.source[field.alias!] + case .mocking: + return nil + } + } + func viewerIsFollowing() throws -> Bool? { let field = GraphQLField.leaf( name: "viewerIsFollowing", @@ -24210,7 +24114,7 @@ extension Fields where TypeLock == Objects.Webhook { } } - func updatedAt() throws -> DateTime { + func updatedAt() throws -> DateTime? { let field = GraphQLField.leaf( name: "updatedAt", arguments: [] @@ -24219,12 +24123,9 @@ extension Fields where TypeLock == Objects.Webhook { switch response { case let .decoding(data): - if let data = data.updatedAt[field.alias!] { - return data - } - throw HttpError.badpayload + return data.updatedAt[field.alias!] case .mocking: - return DateTime.mockValue + return nil } } @@ -29240,7 +29141,7 @@ extension Unions { struct SetFavoriteArticleResult { let __typename: TypeName let errorCodes: [String: [Enums.SetFavoriteArticleErrorCode]] - let favoriteArticle: [String: Objects.Article] + let success: [String: Bool] enum TypeName: String, Codable { case setFavoriteArticleError = "SetFavoriteArticleError" @@ -29265,8 +29166,8 @@ extension Unions.SetFavoriteArticleResult: Decodable { if let value = try container.decode([Enums.SetFavoriteArticleErrorCode]?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } - case "favoriteArticle": - if let value = try container.decode(Objects.Article?.self, forKey: codingKey) { + case "success": + if let value = try container.decode(Bool?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } default: @@ -29282,7 +29183,7 @@ extension Unions.SetFavoriteArticleResult: Decodable { __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!) errorCodes = map["errorCodes"] - favoriteArticle = map["favoriteArticle"] + success = map["success"] } } @@ -29297,7 +29198,7 @@ extension Fields where TypeLock == Unions.SetFavoriteArticleResult { let data = Objects.SetFavoriteArticleError(errorCodes: data.errorCodes) return try setFavoriteArticleError.decode(data: data) case .setFavoriteArticleSuccess: - let data = Objects.SetFavoriteArticleSuccess(favoriteArticle: data.favoriteArticle) + let data = Objects.SetFavoriteArticleSuccess(success: data.success) return try setFavoriteArticleSuccess.decode(data: data) } case .mocking: @@ -30284,6 +30185,160 @@ extension Selection where TypeLock == Never, Type == Never { typealias UnsubscribeResult = Selection } +extension Unions { + struct UpdateEmailResult { + let __typename: TypeName + let email: [String: String] + let errorCodes: [String: [Enums.UpdateEmailErrorCode]] + let verificationEmailSent: [String: Bool] + + enum TypeName: String, Codable { + case updateEmailError = "UpdateEmailError" + case updateEmailSuccess = "UpdateEmailSuccess" + } + } +} + +extension Unions.UpdateEmailResult: 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 "email": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "errorCodes": + if let value = try container.decode([Enums.UpdateEmailErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "verificationEmailSent": + if let value = try container.decode(Bool?.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")!) + + email = map["email"] + errorCodes = map["errorCodes"] + verificationEmailSent = map["verificationEmailSent"] + } +} + +extension Fields where TypeLock == Unions.UpdateEmailResult { + func on(updateEmailError: Selection, updateEmailSuccess: Selection) throws -> Type { + select([GraphQLField.fragment(type: "UpdateEmailError", selection: updateEmailError.selection), GraphQLField.fragment(type: "UpdateEmailSuccess", selection: updateEmailSuccess.selection)]) + + switch response { + case let .decoding(data): + switch data.__typename { + case .updateEmailError: + let data = Objects.UpdateEmailError(errorCodes: data.errorCodes) + return try updateEmailError.decode(data: data) + case .updateEmailSuccess: + let data = Objects.UpdateEmailSuccess(email: data.email, verificationEmailSent: data.verificationEmailSent) + return try updateEmailSuccess.decode(data: data) + } + case .mocking: + return updateEmailError.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias UpdateEmailResult = Selection +} + +extension Unions { + struct UpdateFilterResult { + let __typename: TypeName + let errorCodes: [String: [Enums.UpdateFilterErrorCode]] + let filter: [String: Objects.Filter] + + enum TypeName: String, Codable { + case updateFilterError = "UpdateFilterError" + case updateFilterSuccess = "UpdateFilterSuccess" + } + } +} + +extension Unions.UpdateFilterResult: 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.UpdateFilterErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "filter": + if let value = try container.decode(Objects.Filter?.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"] + filter = map["filter"] + } +} + +extension Fields where TypeLock == Unions.UpdateFilterResult { + func on(updateFilterError: Selection, updateFilterSuccess: Selection) throws -> Type { + select([GraphQLField.fragment(type: "UpdateFilterError", selection: updateFilterError.selection), GraphQLField.fragment(type: "UpdateFilterSuccess", selection: updateFilterSuccess.selection)]) + + switch response { + case let .decoding(data): + switch data.__typename { + case .updateFilterError: + let data = Objects.UpdateFilterError(errorCodes: data.errorCodes) + return try updateFilterError.decode(data: data) + case .updateFilterSuccess: + let data = Objects.UpdateFilterSuccess(filter: data.filter) + return try updateFilterSuccess.decode(data: data) + } + case .mocking: + return updateFilterError.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias UpdateFilterResult = Selection +} + extension Unions { struct UpdateHighlightReplyResult { let __typename: TypeName @@ -32584,6 +32639,28 @@ extension Enums { } } +extension Enums { + /// UpdateEmailErrorCode + enum UpdateEmailErrorCode: String, CaseIterable, Codable { + case badRequest = "BAD_REQUEST" + + case emailAlreadyExists = "EMAIL_ALREADY_EXISTS" + + case unauthorized = "UNAUTHORIZED" + } +} + +extension Enums { + /// UpdateFilterErrorCode + enum UpdateFilterErrorCode: String, CaseIterable, Codable { + case badRequest = "BAD_REQUEST" + + case notFound = "NOT_FOUND" + + case unauthorized = "UNAUTHORIZED" + } +} + extension Enums { /// UpdateHighlightErrorCode enum UpdateHighlightErrorCode: String, CaseIterable, Codable { @@ -33542,9 +33619,11 @@ extension InputObjects { extension InputObjects { struct SaveArticleReadingProgressInput: Encodable, Hashable { + var force: OptionalArgument = .absent() + var id: String - var readingProgressAnchorIndex: Int + var readingProgressAnchorIndex: OptionalArgument = .absent() var readingProgressPercent: Double @@ -33552,13 +33631,15 @@ extension InputObjects { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + if force.hasValue { try container.encode(force, forKey: .force) } try container.encode(id, forKey: .id) - try container.encode(readingProgressAnchorIndex, forKey: .readingProgressAnchorIndex) + if readingProgressAnchorIndex.hasValue { try container.encode(readingProgressAnchorIndex, forKey: .readingProgressAnchorIndex) } try container.encode(readingProgressPercent, forKey: .readingProgressPercent) if readingProgressTopPercent.hasValue { try container.encode(readingProgressTopPercent, forKey: .readingProgressTopPercent) } } enum CodingKeys: String, CodingKey { + case force case id case readingProgressAnchorIndex case readingProgressPercent @@ -33604,31 +33685,31 @@ extension InputObjects { extension InputObjects { struct SaveFilterInput: Encodable, Hashable { - var category: String + var category: OptionalArgument = .absent() var description: OptionalArgument = .absent() var filter: String - var id: OptionalArgument = .absent() - var name: String + var position: OptionalArgument = .absent() + func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(category, forKey: .category) + if category.hasValue { try container.encode(category, forKey: .category) } if description.hasValue { try container.encode(description, forKey: .description) } try container.encode(filter, forKey: .filter) - if id.hasValue { try container.encode(id, forKey: .id) } try container.encode(name, forKey: .name) + if position.hasValue { try container.encode(position, forKey: .position) } } enum CodingKeys: String, CodingKey { case category case description case filter - case id case name + case position } } } @@ -34061,27 +34142,77 @@ extension InputObjects { extension InputObjects { struct SubscribeInput: Encodable, Hashable { - var name: OptionalArgument = .absent() - var subscriptionType: OptionalArgument = .absent() - var url: OptionalArgument = .absent() + var url: String func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - if name.hasValue { try container.encode(name, forKey: .name) } if subscriptionType.hasValue { try container.encode(subscriptionType, forKey: .subscriptionType) } - if url.hasValue { try container.encode(url, forKey: .url) } + try container.encode(url, forKey: .url) } enum CodingKeys: String, CodingKey { - case name case subscriptionType case url } } } +extension InputObjects { + struct UpdateEmailInput: Encodable, Hashable { + var email: String + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(email, forKey: .email) + } + + enum CodingKeys: String, CodingKey { + case email + } + } +} + +extension InputObjects { + struct UpdateFilterInput: Encodable, Hashable { + var category: OptionalArgument = .absent() + + var description: OptionalArgument = .absent() + + var filter: OptionalArgument = .absent() + + var id: String + + var name: OptionalArgument = .absent() + + var position: OptionalArgument = .absent() + + var visible: OptionalArgument = .absent() + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + if category.hasValue { try container.encode(category, forKey: .category) } + if description.hasValue { try container.encode(description, forKey: .description) } + if filter.hasValue { try container.encode(filter, forKey: .filter) } + try container.encode(id, forKey: .id) + if name.hasValue { try container.encode(name, forKey: .name) } + if position.hasValue { try container.encode(position, forKey: .position) } + if visible.hasValue { try container.encode(visible, forKey: .visible) } + } + + enum CodingKeys: String, CodingKey { + case category + case description + case filter + case id + case name + case position + case visible + } + } +} + extension InputObjects { struct UpdateHighlightInput: Encodable, Hashable { var annotation: OptionalArgument = .absent() @@ -34283,8 +34414,12 @@ extension InputObjects { var lastFetchedAt: OptionalArgument = .absent() + var lastFetchedChecksum: OptionalArgument = .absent() + var name: OptionalArgument = .absent() + var scheduledAt: OptionalArgument = .absent() + var status: OptionalArgument = .absent() func encode(to encoder: Encoder) throws { @@ -34292,7 +34427,9 @@ extension InputObjects { if description.hasValue { try container.encode(description, forKey: .description) } try container.encode(id, forKey: .id) if lastFetchedAt.hasValue { try container.encode(lastFetchedAt, forKey: .lastFetchedAt) } + if lastFetchedChecksum.hasValue { try container.encode(lastFetchedChecksum, forKey: .lastFetchedChecksum) } if name.hasValue { try container.encode(name, forKey: .name) } + if scheduledAt.hasValue { try container.encode(scheduledAt, forKey: .scheduledAt) } if status.hasValue { try container.encode(status, forKey: .status) } } @@ -34300,7 +34437,9 @@ extension InputObjects { case description case id case lastFetchedAt + case lastFetchedChecksum case name + case scheduledAt case status } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateReminder.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateReminder.swift index f7ad4d475..8b1378917 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateReminder.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/CreateReminder.swift @@ -1,79 +1 @@ -import Foundation -import Models -import SwiftGraphQL -public enum ReminderItemId { - case clientRequest(id: String) - case link(id: String) - - var linkId: String? { - switch self { - case .clientRequest: - return nil - case let .link(id): - return id - } - } - - var clientRequestId: String? { - switch self { - case .link: - return nil - case let .clientRequest(id): - return id - } - } -} - -public extension DataService { - func createReminder( - reminderItemId: ReminderItemId, - remindAt: Date - ) async throws { - enum MutationResult { - case complete(id: String) - case error(errorCode: Enums.CreateReminderErrorCode) - } - - let selection = Selection { - try $0.on( - createReminderError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) }, - createReminderSuccess: .init { - .complete(id: try $0.reminder(selection: Selection.Reminder { try $0.id() })) - } - ) - } - - let mutation = Selection.Mutation { - try $0.createReminder( - input: InputObjects.CreateReminderInput( - archiveUntil: true, - clientRequestId: OptionalArgument(reminderItemId.clientRequestId), - linkId: OptionalArgument(reminderItemId.linkId), - remindAt: DateTime(from: remindAt), - sendNotification: true - ), - selection: selection - ) - } - - let path = appEnvironment.graphqlPath - let headers = networker.defaultHeaders - - return try await withCheckedThrowingContinuation { continuation in - send(mutation, to: path, headers: headers) { queryResult in - guard let payload = try? queryResult.get() else { - continuation.resume(throwing: BasicError.message(messageText: "network error")) - return - } - - switch payload.data { - case .complete: - continuation.resume() - case let .error(errorCode: errorCode): - continuation.resume(throwing: BasicError.message(messageText: errorCode.rawValue)) - } - } - } - } -} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleReadingProgress.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleReadingProgress.swift index 41781b70e..100109009 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleReadingProgress.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleReadingProgress.swift @@ -4,13 +4,15 @@ import Models import SwiftGraphQL extension DataService { - public func updateLinkReadingProgress(itemID: String, readingProgress: Double, anchorIndex: Int) { + public func updateLinkReadingProgress(itemID: String, readingProgress: Double, anchorIndex: Int, force: Bool?) { backgroundContext.perform { [weak self] in guard let self = self else { return } guard let linkedItem = LinkedItem.lookup(byID: itemID, inContext: self.backgroundContext) else { return } - if readingProgress != 0, readingProgress < linkedItem.readingProgress { - return + if let force = force, !force { + if readingProgress != 0, readingProgress < linkedItem.readingProgress { + return + } } print("updating reading progress: ", readingProgress, anchorIndex) @@ -24,12 +26,13 @@ extension DataService { self.syncLinkReadingProgress( itemID: linkedItem.unwrappedID, readingProgress: readingProgress, - anchorIndex: anchorIndex + anchorIndex: anchorIndex, + force: force ) } } - func syncLinkReadingProgress(itemID: String, readingProgress: Double, anchorIndex: Int) { + func syncLinkReadingProgress(itemID: String, readingProgress: Double, anchorIndex: Int, force: Bool?) { enum MutationResult { case saved(readAt: Date?) case error(errorCode: Enums.SaveArticleReadingProgressErrorCode) @@ -49,8 +52,9 @@ extension DataService { let mutation = Selection.Mutation { try $0.saveArticleReadingProgress( input: InputObjects.SaveArticleReadingProgressInput( + force: OptionalArgument(force), id: itemID, - readingProgressAnchorIndex: anchorIndex, + readingProgressAnchorIndex: OptionalArgument(anchorIndex), readingProgressPercent: readingProgress ), selection: selection diff --git a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift index 7c2b94693..6f50cbd8d 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift @@ -154,7 +154,8 @@ public extension DataService { syncLinkReadingProgress( itemID: item.unwrappedID, readingProgress: item.readingProgress, - anchorIndex: Int(item.readingProgressAnchor) + anchorIndex: Int(item.readingProgressAnchor), + force: item.isPDF ) } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift index a1427b35a..f654003f3 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift @@ -26,7 +26,7 @@ extension DataService { createdAt: try $0.createdAt().value ?? Date(), savedAt: try $0.savedAt().value ?? Date(), readAt: try $0.readAt()?.value, - updatedAt: try $0.updatedAt().value ?? Date(), + updatedAt: try $0.updatedAt()?.value ?? Date(), state: try $0.state()?.rawValue.asArticleContentStatus ?? .succeeded, readingProgress: try $0.readingProgressPercent(), readingProgressAnchor: try $0.readingProgressAnchorIndex(), diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift index 548ba02ea..9c1dd7a98 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift @@ -258,7 +258,7 @@ private let libraryArticleSelection = Selection.Article { createdAt: try $0.createdAt().value ?? Date(), savedAt: try $0.savedAt().value ?? Date(), readAt: try $0.readAt()?.value, - updatedAt: try $0.updatedAt().value ?? Date(), + updatedAt: try $0.updatedAt()?.value ?? Date(), state: try $0.state()?.rawValue.asArticleContentStatus ?? .succeeded, readingProgress: try $0.readingProgressPercent(), readingProgressAnchor: try $0.readingProgressAnchorIndex(), diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/SubscriptionsQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/SubscriptionsQuery.swift index a78045904..17ee2d565 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/SubscriptionsQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/SubscriptionsQuery.swift @@ -19,7 +19,7 @@ public extension DataService { status: try SubscriptionStatus.make(from: $0.status()), unsubscribeHttpUrl: try $0.unsubscribeHttpUrl(), unsubscribeMailTo: try $0.unsubscribeMailTo(), - updatedAt: try $0.updatedAt().value, + updatedAt: try $0.updatedAt()?.value ?? Date(), url: try $0.url(), icon: try $0.icon() ) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Selections/HighlightSelection.swift b/apple/OmnivoreKit/Sources/Services/DataService/Selections/HighlightSelection.swift index b9e9e53f8..52e733239 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Selections/HighlightSelection.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Selections/HighlightSelection.swift @@ -1,3 +1,4 @@ +import Foundation import Models import SwiftGraphQL @@ -22,7 +23,7 @@ let highlightSelection = Selection.Highlight { patch: try $0.patch() ?? "", annotation: try $0.annotation(), createdAt: try $0.createdAt().value, - updatedAt: try $0.updatedAt().value, + updatedAt: try $0.updatedAt()?.value ?? Date(), createdByMe: try $0.createdByMe(), createdBy: try $0.user(selection: userProfileSelection), positionPercent: try $0.highlightPositionPercent(), diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/fr-FR-DeniseNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/fr-FR-DeniseNeural.mp3 new file mode 100644 index 000000000..a7d2bfcf6 Binary files /dev/null and b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/fr-FR-DeniseNeural.mp3 differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/fr-FR-HenriNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/fr-FR-HenriNeural.mp3 new file mode 100644 index 000000000..e8602e018 Binary files /dev/null and b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/fr-FR-HenriNeural.mp3 differ diff --git a/apple/gql-server/package.json b/apple/gql-server/package.json index 9088164b4..c6feb1a42 100644 --- a/apple/gql-server/package.json +++ b/apple/gql-server/package.json @@ -10,5 +10,8 @@ "express": "^4.18.1", "express-graphql": "^0.12.0", "graphql": "^16.8.1" + }, + "volta": { + "extends": "../../package.json" } -} \ No newline at end of file +} diff --git a/docs/guides/getting-started/hi/getting-started-guide.md b/docs/guides/getting-started/hi/getting-started-guide.md new file mode 100644 index 000000000..1c781ec2b --- /dev/null +++ b/docs/guides/getting-started/hi/getting-started-guide.md @@ -0,0 +1,158 @@ +# ओम्निवोर के साथ शुरुआत करना + +ओम्निवोर एक ***रीड-इट-लेटर*** ऐप है जो आपको ऑनलाइन पढ़ी गई हर चीज को सहेजने और व्यवस्थित करने की सुविधा देता है। + +यह मार्गदर्शिका आपको दिखाएगी कि ओम्निवोर के बुनियादी कार्यों और उन्नत सुविधाओं का उपयोग कैसे करें, जो चार मुख्य गतिविधियों में विभाजित हैं: + +- सहेजा जा रहा है +- पढ़ना +- आयोजन +- एकीकरण + +लाइब्रेरी आपके सर्वभक्षी अनुभव का केंद्र है, जहां आप अपने द्वारा सहेजे गए किसी भी लिंक तक तुरंत पहुंच सकते हैं। सहेजे गए लिंक आपकी लाइब्रेरी में हमेशा के लिए रहते हैं जब तक कि आप उन्हें हटा नहीं देते। + +## सहेजें + +उन पृष्ठों या लेखों के लिंक सहेजने के पाँच मुख्य तरीके हैं जिन्हें आप बाद में पढ़ना चाहते हैं: + +- आपकी सर्वभक्षी लाइब्रेरी से बचत +- ब्राउज़र से सहेजा जा रहा है +- फ़ोन या टैबलेट से बचत (iOS या Android) +- ईमेल के माध्यम से न्यूज़लेटर सदस्यताएँ +- मैक से पीडीएफ़ सहेजना + +### आपकी सर्वभक्षी लाइब्रेरी से बचत +1. अपनी लाइब्रेरी के ऊपरी दाएं कोने में, लिंक जोड़ें बटन पर टैप करें। +2. वह यूआरएल दर्ज करें जिसे आप सहेजना चाहते हैं और लिंक जोड़ें पर टैप करें। +3. अगली बार जब आप इसे रीफ्रेश करेंगे तो लिंक आपकी लाइब्रेरी में दिखाई देगा। + +### ब्राउज़र से सहेजा जा रहा है + +1. अपने ब्राउज़र के लिए ओम्निवोर एक्सटेंशन डाउनलोड और इंस्टॉल करें: + +- [ क्रोम ](https://omnivore.app/install/chrome) +- [ एज ](https://omnivore.app/install/edge) +- [ फ़ायरफ़ॉक्स ](https://omnivore.app/install/firefox) +- [ सफारी ](https://omnivore.app/install/safari) + +2. उस पृष्ठ पर जाएँ जिसे आप सहेजना चाहते हैं और अपने ब्राउज़र के टूलबार या एक्सटेंशन मेनू में ओम्निवोर बटन पर टैप करें। +3. वैकल्पिक रूप से, आप किसी भी हाइपरलिंक पर राइट-क्लिक (मैक पर कमांड+क्लिक) कर सकते हैं और मेनू से सेव टू ओम्निवोर का चयन कर सकते हैं। +4. अगली बार जब आप इसे रीफ्रेश करेंगे तो लिंक आपकी लाइब्रेरी में दिखाई देगा। + +### फ़ोन या टेबलेट से सहेजा जा रहा है + +अपने मोबाइल डिवाइस से लिंक सहेजने का सबसे अच्छा तरीका ओम्निवोर ऐप है। आप यहां ऐप डाउनलोड कर सकते हैं: + +- [ आईओएस (आईफोन या आईपैड) ](https://omnivore.app/install/ios) +- एंड्रॉयड + +एक बार मोबाइल ऐप इंस्टॉल हो जाए: + +1. अपने ब्राउज़र में, उस पृष्ठ पर जाएँ जिसे आप सहेजना चाहते हैं और शेयर बटन पर टैप करें। +2. शेयर मेनू में सर्वाहारी आइकन पर टैप करें। +3. अगली बार जब आप इसे रीफ्रेश करेंगे तो लिंक आपकी लाइब्रेरी में दिखाई देगा। + +### ईमेल के माध्यम से न्यूज़लेटर सदस्यताएँ + +1. ओम्निवोर वेबसाइट या ऐप पर, प्रोफ़ाइल मेनू तक पहुंचने के लिए ऊपरी दाएं कोने में अपनी फोटो, नाम के पहले अक्षर या अवतार पर टैप करें। मेनू से ईमेल चुनें. +2. सूची में नया ईमेल पता (उदा: username-123_abc@inbox.omnivore.app) जोड़ने के लिए नया ईमेल पता बनाएं पर टैप करें। +3. ईमेल पते के आगे कॉपी आइकन पर क्लिक करें। +4. जिस न्यूज़लेटर की आप सदस्यता लेना चाहते हैं उसके लिए साइनअप पृष्ठ पर जाएँ। +5. ओम्निवोर ईमेल पते को साइनअप फॉर्म में चिपकाएँ। +6. नए न्यूज़लेटर स्वचालित रूप से आपके ओमनिवोर इनबॉक्स में वितरित किए जाएंगे। + +### मैक से पीडीएफ़ सहेजना + +1. मैक ऐप इंस्टॉल करें। [मैक ऐप](https://omnivore.app/install/mac) +2. अपने मैक पर, वह पीडीएफ ढूंढें जिसे आप सहेजना चाहते हैं और फ़ाइल नाम पर राइट-क्लिक करें या Ctrl+click करें। +3. मेनू से शेयर चुनें और ओम्निवोर चुनें। +4. गली बार जब आप इसे रीफ्रेश करेंगे तो लिंक आपकी लाइब्रेरी में दिखाई देगा। + +## पढ़ना + +रीडर दृश्य में प्रवेश करने के लिए अपनी लाइब्रेरी में सहेजे गए किसी भी लिंक पर क्लिक करें। + +आसानी से पढ़ने और हाइलाइट करने के लिए, विकर्षण-मुक्त पढ़ने के लिए विज्ञापनों और अव्यवस्थाओं को हटाने के लिए ओमनिवोर पृष्ठों को प्रारूपित करता है। टेक्स्ट-केंद्रित दृश्य लेखों को छोटा और लोड करने में तेज़ बनाता है। + +पढ़ते समय, आप यह कर सकते हैं: + +- फ़ॉर्मेटिंग बदलें +- टेक्स्ट हाइलाइट करें +- नोट्स जोड़ें +- सभी सहेजे गए हाइलाइट्स और नोट्स देखें +- ट्रैक रीडिंग प्रोग्रेस + +### फ़ॉर्मेटिंग बदलें + +1. **_थीम:_** प्रोफ़ाइल मेनू तक पहुंचने के लिए ऊपरी दाएं कोने में अपनी फ़ोटो, नाम के पहले अक्षर या अवतार पर टैप करें। लाइट या डार्क थीम चुनने के लिए सफेद या काले थंबनेल का चयन करें। +2. **_टेक्स्ट फ़ॉर्मेटिंग:_** टेक्स्ट आकार, फ़ॉन्ट, मार्जिन और लाइन स्पेसिंग को समायोजित करने के लिए एए आइकन पर टैप करें। + +### टेक्स्ट हाइलाइट करें + +1. वह टेक्स्ट चुनें जिसे आप हाइलाइट करना चाहते हैं. +2. **हाइलाइट** बटन पर टैप करें। +3. अगली बार जब आप लेख देखेंगे तो पाठ हाइलाइट किया हुआ दिखाई देगा। + +### नोट्स जोड़ें +1. टेक्स्ट के उस भाग को हाइलाइट करें जहाँ आप नोट जोड़ना चाहते हैं। +2. **नोट** बटन पर टैप करें, अपना नोट टाइप करें और सेव पर टैप करें। +3. अगली बार जब आप यह लेख देखेंगे तो नोट आइकन दिखाई देगा। + +### सभी सहेजे गए हाइलाइट्स और नोट्स देखें +1. इस पृष्ठ पर आपके द्वारा जोड़े गए सभी हाइलाइट किए गए टेक्स्ट और नोट्स की सूची देखने के लिए हाइलाइट/नोट आइकन पर टैप करें। +2. किसी नोट या हाइलाइट को हटाने के लिए, उसे सूची से चुनें और ट्रैश आइकन पर टैप करें। + +### ट्रैक रीडिंग प्रोग्रेस + +ओम्निवोर स्वचालित रूप से आपके विभिन्न उपकरणों पर आपकी पढ़ने की प्रगति पर नज़र रखता है ताकि आप आसानी से वहीं से शुरू कर सकें जहाँ आपने छोड़ा था। आपके पढ़ने शुरू करने के बाद आपकी लाइब्रेरी में प्रत्येक लिंक के शीर्ष पर एक प्रगति बार दिखाई देगा। + +## आयोजन + +डिफ़ॉल्ट रूप से, लाइब्रेरी इनबॉक्स आपके द्वारा सहेजे गए सभी लिंक प्रदर्शित करता है। आपकी सूची प्रबंधित करने और आपके पढ़ने को व्यवस्थित रखने के लिए, ओम्निवोर निम्नलिखित क्रियाएं प्रदान करता है: + +- संग्रह +- लेबल +- खोज +- फिल्टर + +### संग्रह + +1. जिस लिंक को आप संग्रहित करना चाहते हैं उसके बगल में स्थित मेनू आइकन पर टैप करें (मोबाइल ऐप पर, मेनू खोलने के लिए लिंक को देर तक दबाएं)। +2. **आर्किव** चुनें. +3. लिंक डिफ़ॉल्ट लाइब्रेरी दृश्य से गायब हो जाएगा, लेकिन यदि आप संग्रहीत फ़िल्टर का चयन करते हैं तो दिखाई देगा (नीचे फ़िल्टर देखें)। + +### लेबल + +1. किसी भी लिंक के आगे मेनू आइकन टैप करें और लेबल सेट करें चुनें। +2. सूची से किसी मौजूदा लेबल का चयन करें या नया लेबल बनाने के लिए लेबल संपादित करें पर टैप करें। +3. लेबल आपकी लाइब्रेरी में लिंक के बगल में दिखाई देगा। समान लेबल वाले सभी लिंक देखने के लिए इसे टैप करें। +4. केवल ओमनिवोर मोबाइल ऐप: आपके द्वारा उपयोग किए गए सभी लेबलों की पूरी सूची देखने के लिए **लेबल** पर टैप करें; समान लेबल वाले सभी लिंक देखने के लिए एक टैप करें +5. ध्यान दें: ओमनिवोर स्वचालित रूप से कुछ लेबल निर्दिष्ट करेगा, जैसे "न्यूज़लेटर्स।" + +### खोज + +1. अपने सभी सहेजे गए लिंक को खोजने के लिए, खोज बार में एक कीवर्ड या वाक्यांश दर्ज करें। +2. आप अपनी खोज को और भी अधिक केंद्रित करने के लिए कीवर्ड को लेबल और फ़िल्टर के साथ जोड़ सकते हैं। उन्नत खोज के बारे में और जानें. + +### फिल्टर + +1. अपने लाइब्रेरी दृश्य को परिष्कृत करने के लिए **फ़िल्टर** मेनू का उपयोग करें (कुछ फ़िल्टर डिफ़ॉल्ट रूप से दिखाई दे सकते हैं)। +2. न्यूज़लेटर्स को छोड़कर अपने सभी गैर-संग्रहीत लिंक की सूची देखने के लिए बाद में पढ़ें का चयन करें। +3. आपके द्वारा अपने सभी सहेजे गए पृष्ठों में हाइलाइट किए गए टेक्स्ट चयन को देखने के लिए हाइलाइट्स का चयन करें। +4. आज आपके द्वारा सहेजे गए लिंक की सूची देखने के लिए आज का चयन करें। +5. अपने न्यूज़लेटर सदस्यता के माध्यम से सहेजे गए लिंक देखने के लिए न्यूज़लेटर्स का चयन करें। + +## एकीकरण + +ओमनिवोर ज्ञान आधारों और नोट लेने वाले ऐप्स के साथ एकीकरण की अनुमति देता है जिनमें शामिल हैं: + +- लोगसेक +- वेबहुक + +### लॉगसेक + +ओमनिवोर के लॉगसेक प्लगइन के साथ आप अपने सभी सहेजे गए लेख, हाइलाइट्स और नोट्स को एक लोकप्रिय ज्ञान आधार लॉगसेक में सिंक कर सकते हैं। लॉगसेक प्लगइन की स्थापना और उपयोग के बारे में जानकारी के लिए, कृपया इस सहायक को देखें [Omnivore for Logseq Plugin Guide](https://briansunter.com/graph/#/page/omnivore-logseq-guide). + +### वेबहुक + +जब आप कोई लिंक सहेजते हैं या जिस पृष्ठ को आप पढ़ रहे हैं उसमें हाइलाइट्स जोड़ते हैं तो ओम्निवोर वेबहुक को ट्रिगर कर सकता है। यह उदाहरण Google ड्राइव पर संग्रहीत Google शीट स्प्रेडशीट में सभी सहेजे गए लिंक लिखने के लिए वेबहुक का उपयोग किया जा रहा है। \ No newline at end of file diff --git a/lerna.json b/lerna.json index dabb023cd..1a3cacb6d 100644 --- a/lerna.json +++ b/lerna.json @@ -3,6 +3,5 @@ "packages/*" ], "version": "1.0.0", - "npmClient": "yarn", - "useWorkspaces": true -} + "npmClient": "yarn" +} \ No newline at end of file diff --git a/package.json b/package.json index 55b813a4b..aad1ec9a4 100644 --- a/package.json +++ b/package.json @@ -6,16 +6,16 @@ "workspaces": [ "packages/*" ], - "license": "UNLICENSED", + "license": "AGPL-3.0-only", "scripts": { - "test": "lerna run --no-bail test --ignore @omnivore/web", - "lint": "lerna run lint --ignore @omnivore/web", - "build": "lerna run build --ignore @omnivore/web", - "bootstrap": "lerna bootstrap", + "test": "lerna run --no-bail test", + "lint": "lerna run lint", + "build": "lerna run build", "test:scoped:example": "lerna run test --scope={@omnivore/pdf-handler,@omnivore/web}", "gql-typegen": "graphql-codegen", "deploy:web": "vercel --prod" }, + "dependencies": {}, "devDependencies": { "@ardatan/aggregate-error": "^0.0.6", "@graphql-codegen/cli": "^2.6.2", @@ -31,13 +31,12 @@ "eslint-plugin-prettier": "^4.0.0", "graphql": "^15.3.0", "graphql-tag": "^2.11.0", - "lerna": "^4.0.0", + "lerna": "^7.4.1", "prettier": "^2.5.1", - "typescript": "^4.4.3" + "typescript": "4.5.2" }, "volta": { "node": "18.16.1", "yarn": "1.22.19" - }, - "dependencies": {} -} \ No newline at end of file + } +} diff --git a/packages/api/Dockerfile b/packages/api/Dockerfile index 1a8254361..674c1d396 100644 --- a/packages/api/Dockerfile +++ b/packages/api/Dockerfile @@ -1,9 +1,9 @@ -FROM node:18.16-alpine as builder +FROM node:18.16 as builder WORKDIR /app ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true -RUN apk add g++ make python3 +RUN apt-get update && apt-get install -y g++ make python3 COPY package.json . COPY yarn.lock . @@ -32,7 +32,9 @@ RUN rm -rf /app/packages/api/node_modules RUN rm -rf /app/node_modules RUN yarn install --pure-lockfile --production -FROM node:18.16-alpine as runner +FROM node:18.16 as runner + +RUN apt-get update && apt-get install -y netcat-openbsd WORKDIR /app diff --git a/packages/api/package.json b/packages/api/package.json index e2132082b..d4801f5ed 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -8,6 +8,7 @@ "start": "node dist/server.js", "lint": "eslint src --ext ts,js,tsx,jsx", "lint:fix": "eslint src --fix --ext ts,js,tsx,jsx", + "test:typecheck": "tsc --noEmit", "test": "nyc mocha -r ts-node/register --config mocha-config.json --timeout 10000", "copy-files": "copyfiles -u 1 src/**/*.html dist/" }, @@ -145,7 +146,6 @@ "node": "18.16.1" }, "volta": { - "node": "18.16.1", - "yarn": "1.22.19" + "extends": "../../package.json" } } diff --git a/packages/api/src/repository/index.ts b/packages/api/src/repository/index.ts index 7b02d12a2..9e1dd187d 100644 --- a/packages/api/src/repository/index.ts +++ b/packages/api/src/repository/index.ts @@ -2,6 +2,7 @@ import * as httpContext from 'express-http-context2' import { EntityManager, EntityTarget, Repository } from 'typeorm' import { appDataSource } from '../data_source' import { Claims } from '../resolvers/types' +import { SetClaimsRole } from '../utils/dictionary' export const getColumns = (repository: Repository): (keyof T)[] => { return repository.metadata.columns.map( @@ -12,8 +13,10 @@ export const getColumns = (repository: Repository): (keyof T)[] => { export const setClaims = async ( manager: EntityManager, uid = '00000000-0000-0000-0000-000000000000', - dbRole = 'omnivore_user' + userRole = 'user' ): Promise => { + const dbRole = + userRole === SetClaimsRole.ADMIN ? 'omnivore_admin' : 'omnivore_user' return manager.query('SELECT * from omnivore.set_claims($1, $2)', [ uid, dbRole, diff --git a/packages/api/src/routers/svc/user.ts b/packages/api/src/routers/svc/user.ts new file mode 100644 index 000000000..ca6e9dbea --- /dev/null +++ b/packages/api/src/routers/svc/user.ts @@ -0,0 +1,70 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import cors from 'cors' +import express from 'express' +import { LessThan } from 'typeorm' +import { StatusType } from '../../entity/user' +import { readPushSubscription } from '../../pubsub' +import { deleteUsers } from '../../services/user' +import { corsConfig } from '../../utils/corsConfig' +import { logger } from '../../utils/logger' + +type CleanupMessage = { + subDays: number +} + +const isCleanupMessage = (obj: any): obj is CleanupMessage => + 'subDays' in obj && !isNaN(obj.subDays) + +const getCleanupMessage = (msgStr: string): CleanupMessage => { + try { + const obj = JSON.parse(msgStr) as unknown + if (isCleanupMessage(obj)) { + return obj + } + } catch (err) { + console.log('error deserializing event: ', { msgStr, err }) + } + + return { + subDays: 0, // default to 0 + } +} + +export function userServiceRouter() { + const router = express.Router() + + router.post('/prune', cors(corsConfig), async (req, res) => { + logger.info('prune soft deleted users') + + const { message: msgStr, expired } = readPushSubscription(req) + + if (!msgStr) { + return res.status(200).send('Bad Request') + } + + if (expired) { + logger.info('discarding expired message') + return res.status(200).send('Expired') + } + + const cleanupMessage = getCleanupMessage(msgStr) + const subTime = cleanupMessage.subDays * 1000 * 60 * 60 * 24 // convert days to milliseconds + + try { + const result = await deleteUsers({ + status: StatusType.Deleted, + updatedAt: LessThan(new Date(Date.now() - subTime)), // subDays ago + }) + logger.info('prune result', result) + + return res.sendStatus(200) + } catch (error) { + logger.error('error prune users', error) + + return res.sendStatus(500) + } + }) + + return router +} diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index b40a82bb8..1209deb9e 100755 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -31,6 +31,7 @@ import { newsletterServiceRouter } from './routers/svc/newsletters' // import { remindersServiceRouter } from './routers/svc/reminders' import { rssFeedRouter } from './routers/svc/rss_feed' import { uploadServiceRouter } from './routers/svc/upload' +import { userServiceRouter } from './routers/svc/user' import { webhooksServiceRouter } from './routers/svc/webhooks' import { textToSpeechRouter } from './routers/text_to_speech' import { userRouter } from './routers/user_router' @@ -121,6 +122,7 @@ export const createApp = (): { app.use('/svc/pubsub/webhooks', webhooksServiceRouter()) app.use('/svc/pubsub/integrations', integrationsServiceRouter()) app.use('/svc/pubsub/rss-feed', rssFeedRouter()) + app.use('/svc/pubsub/user', userServiceRouter()) // app.use('/svc/reminders', remindersServiceRouter()) app.use('/svc/email-attachment', emailAttachmentRouter()) diff --git a/packages/api/src/services/integrations/readwise.ts b/packages/api/src/services/integrations/readwise.ts index 0c6676ebd..cfbbf421e 100644 --- a/packages/api/src/services/integrations/readwise.ts +++ b/packages/api/src/services/integrations/readwise.ts @@ -148,6 +148,8 @@ export class ReadwiseIntegration extends IntegrationService { ) return response.status === 200 } catch (error) { + logger.error(error) + if (axios.isAxiosError(error)) { if (error.response?.status === 429 && retryCount < 3) { logger.info('Readwise API rate limit exceeded, retrying...') @@ -157,10 +159,6 @@ export class ReadwiseIntegration extends IntegrationService { await wait(parseInt(retryAfter, 10) * 1000) return this.syncWithReadwise(token, highlights, retryCount + 1) } - - logger.error(error.message) - } else { - logger.error(error) } return false diff --git a/packages/api/src/services/user.ts b/packages/api/src/services/user.ts index 870d9ce6b..f3d01e489 100644 --- a/packages/api/src/services/user.ts +++ b/packages/api/src/services/user.ts @@ -1,6 +1,8 @@ +import { DeepPartial, FindOptionsWhere, In } from 'typeorm' import { StatusType, User } from '../entity/user' import { authTrx } from '../repository' import { userRepository } from '../repository/user' +import { SetClaimsRole } from '../utils/dictionary' export const deleteUser = async (userId: string) => { await authTrx( @@ -20,6 +22,28 @@ export const updateUser = async (userId: string, update: Partial) => { ) } -export const findUser = async (id: string): Promise => { +export const findActiveUser = async (id: string): Promise => { return userRepository.findOneBy({ id, status: StatusType.Active }) } + +export const findUsersById = async (ids: string[]): Promise => { + return userRepository.findBy({ id: In(ids) }) +} + +export const deleteUsers = async (criteria: FindOptionsWhere) => { + return authTrx( + async (t) => t.getRepository(User).delete(criteria), + undefined, + undefined, + SetClaimsRole.ADMIN + ) +} + +export const createUsers = async (users: DeepPartial[]) => { + return authTrx( + async (t) => t.getRepository(User).save(users), + undefined, + undefined, + SetClaimsRole.ADMIN + ) +} diff --git a/packages/api/test/resolvers/user.test.ts b/packages/api/test/resolvers/user.test.ts index 82e0ceefc..d3ac7ea4f 100644 --- a/packages/api/test/resolvers/user.test.ts +++ b/packages/api/test/resolvers/user.test.ts @@ -6,7 +6,7 @@ import { UpdateUserProfileErrorCode, } from '../../src/generated/graphql' import { findProfile } from '../../src/services/profile' -import { deleteUser, findUser } from '../../src/services/user' +import { deleteUser, findActiveUser } from '../../src/services/user' import { hashPassword } from '../../src/utils/auth' import { createTestUser } from '../db' import { generateFakeUuid, graphqlRequest, request } from '../util' @@ -98,7 +98,7 @@ describe('User API', () => { it('updates user and responds with status code 200', async () => { const response = await graphqlRequest(query, authToken).expect(200) - const user = await findUser(response.body.data.updateUser.user.id) + const user = await findActiveUser(response.body.data.updateUser.user.id) expect(user?.name).to.eql(name) }) }) diff --git a/packages/api/test/routers/user.test.ts b/packages/api/test/routers/user.test.ts new file mode 100644 index 000000000..d6ca4a003 --- /dev/null +++ b/packages/api/test/routers/user.test.ts @@ -0,0 +1,65 @@ +import { expect } from 'chai' +import 'mocha' +import { In } from 'typeorm' +import { StatusType } from '../../src/entity/user' +import { + createUsers, + deleteUsers, + findUsersById, +} from '../../src/services/user' +import { request } from '../util' + +describe('User Service Router', () => { + const token = process.env.PUBSUB_VERIFICATION_TOKEN || '' + + describe('prune', () => { + let toDeleteUserIds: string[] = [] + + before(async () => { + // create test users + const users = await createUsers([ + { + name: 'user_1', + email: 'user_1@omnivore.app', + status: StatusType.Deleted, + updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2), // 2 days ago + source: 'GOOGLE', + sourceUserId: '123', + }, + { + name: 'user_2', + email: 'user_2@omnivore.app', + status: StatusType.Deleted, + updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2), // 2 days ago + source: 'GOOGLE', + sourceUserId: '456', + }, + ]) + toDeleteUserIds = users.map((u) => u.id) + }) + + after(async () => { + // delete test users + await deleteUsers({ id: In(toDeleteUserIds) }) + }) + + it('prunes soft deleted users a day ago', async () => { + const data = { + message: { + data: Buffer.from( + JSON.stringify({ subDays: 1 }) // 1 day ago + ).toString('base64'), + publishTime: new Date().toISOString(), + }, + } + + await request + .post('/svc/pubsub/user/prune?token=' + token) + .send(data) + .expect(200) + + const deletedUsers = await findUsersById(toDeleteUserIds) + expect(deletedUsers.length).to.equal(0) + }) + }) +}) diff --git a/packages/appreader/package.json b/packages/appreader/package.json index 00cf01662..7be7aa89a 100644 --- a/packages/appreader/package.json +++ b/packages/appreader/package.json @@ -32,7 +32,6 @@ "webpack-dev-server": "^4.7.4" }, "volta": { - "node": "18.16.0", - "yarn": "1.22.10" + "extends": "../../package.json" } -} \ No newline at end of file +} diff --git a/packages/appreader/webpack.config.ts b/packages/appreader/webpack.config.ts index 472ab0d37..26579bb5b 100644 --- a/packages/appreader/webpack.config.ts +++ b/packages/appreader/webpack.config.ts @@ -60,6 +60,11 @@ const config: Configuration = { ], resolve: { extensions: ['.tsx', '.ts', '.js'], + fallback: { + stream: false, + fs: false, + zlib: false, + } }, output: { path: path.resolve(__dirname, 'build'), diff --git a/packages/content-fetch/Dockerfile b/packages/content-fetch/Dockerfile index 4eb444d74..a637ce9b0 100644 --- a/packages/content-fetch/Dockerfile +++ b/packages/content-fetch/Dockerfile @@ -1,13 +1,9 @@ -FROM node:18.16-alpine +FROM node:18.16 # Installs latest Chromium (92) package. -RUN apk add --no-cache \ +RUN apt-get update && apt-get install -y \ chromium \ - nss \ - freetype \ - harfbuzz \ ca-certificates \ - ttf-freefont \ nodejs \ yarn \ g++ \ @@ -16,7 +12,7 @@ RUN apk add --no-cache \ WORKDIR /app -ENV CHROMIUM_PATH /usr/bin/chromium-browser +ENV CHROMIUM_PATH /usr/bin/chromium ENV LAUNCH_HEADLESS=true COPY package.json . diff --git a/packages/content-fetch/package.json b/packages/content-fetch/package.json index 546552258..a0521cbee 100644 --- a/packages/content-fetch/package.json +++ b/packages/content-fetch/package.json @@ -20,5 +20,8 @@ "start_gcf": "npx functions-framework --port=9090 --target=puppeteer", "start_preview": "npx functions-framework --target=preview", "test": "mocha test/*.js" + }, + "volta": { + "extends": "../../package.json" } } diff --git a/packages/content-handler/package.json b/packages/content-handler/package.json index adda03962..55ead0886 100644 --- a/packages/content-handler/package.json +++ b/packages/content-handler/package.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "scripts": { "test": "yarn mocha -r ts-node/register --config mocha-config.json", + "test:typecheck": "tsc --noEmit", "lint": "eslint src --ext ts,js,tsx,jsx", "compile": "tsc", "build": "tsc" @@ -38,5 +39,8 @@ "redis": "^4.3.1", "underscore": "^1.13.6", "uuid": "^9.0.0" + }, + "volta": { + "extends": "../../package.json" } } diff --git a/packages/content-handler/src/websites/twitter-handler.ts b/packages/content-handler/src/websites/twitter-handler.ts index d80eaacf7..e8457c3d7 100644 --- a/packages/content-handler/src/websites/twitter-handler.ts +++ b/packages/content-handler/src/websites/twitter-handler.ts @@ -228,7 +228,7 @@ const getTweetIds = async ( timeout: 60000, // 60 seconds }) - return (await page.evaluate(async (author) => { + return await page.evaluate(async (author) => { /** * Wait for `ms` amount of milliseconds * @param {number} ms @@ -278,7 +278,7 @@ const getTweetIds = async ( } return ids - }, author)) as string[] + }, author) } catch (error) { console.error('Error getting tweets', error) diff --git a/packages/cypress/package.json b/packages/cypress/package.json index 47d5818a8..cba74d085 100644 --- a/packages/cypress/package.json +++ b/packages/cypress/package.json @@ -11,7 +11,6 @@ }, "devDependencies": {}, "volta": { - "node": "18.16.1", - "yarn": "1.22.10" + "extends": "../../package.json" } -} \ No newline at end of file +} diff --git a/packages/db/Dockerfile b/packages/db/Dockerfile index 744ece0e7..b69e911c4 100644 --- a/packages/db/Dockerfile +++ b/packages/db/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18.16-alpine +FROM node:18.16 WORKDIR /app @@ -8,7 +8,9 @@ COPY tsconfig.json . COPY /packages/db/package.json ./packages/db/package.json -RUN apk --no-cache --virtual build-dependencies add postgresql uuidgen +RUN apt-get update && apt-get install -y \ + postgresql \ + uuid-runtime RUN yarn install diff --git a/packages/db/migrations/0141.do.add_index_for_cleanup_to_user.sql b/packages/db/migrations/0141.do.add_index_for_cleanup_to_user.sql new file mode 100755 index 000000000..3936b9415 --- /dev/null +++ b/packages/db/migrations/0141.do.add_index_for_cleanup_to_user.sql @@ -0,0 +1,9 @@ +-- Type: DO +-- Name: add_index_for_cleanup_to_user +-- Description: Add index of status and updated_at to omnivore.user table for cleanup of deleted users + +BEGIN; + +CREATE INDEX IF NOT EXISTS user_status_updated_at_idx ON omnivore.user (status, updated_at); + +COMMIT; diff --git a/packages/db/migrations/0141.undo.add_index_for_cleanup_to_user.sql b/packages/db/migrations/0141.undo.add_index_for_cleanup_to_user.sql new file mode 100755 index 000000000..cee6bae43 --- /dev/null +++ b/packages/db/migrations/0141.undo.add_index_for_cleanup_to_user.sql @@ -0,0 +1,9 @@ +-- Type: UNDO +-- Name: add_index_for_cleanup_to_user +-- Description: Add index of status and updated_at to omnivore.user table for cleanup of deleted users + +BEGIN; + +DROP INDEX IF EXISTS user_status_updated_at_idx; + +COMMIT; diff --git a/packages/db/migrations/0142.do.create_omnivore_admin_role.sql b/packages/db/migrations/0142.do.create_omnivore_admin_role.sql new file mode 100755 index 000000000..22de188c7 --- /dev/null +++ b/packages/db/migrations/0142.do.create_omnivore_admin_role.sql @@ -0,0 +1,19 @@ +-- Type: DO +-- Name: create_omnivore_admin_role +-- Description: Create omnivore_admin role with admin permissions + +BEGIN; + +CREATE ROLE omnivore_admin; + +GRANT omnivore_admin TO app_user; + +GRANT ALL PRIVILEGES ON SCHEMA omnivore TO omnivore_admin; +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA omnivore TO omnivore_admin; + +CREATE POLICY user_admin_policy on omnivore.user + FOR ALL + TO omnivore_admin + USING (true); + +COMMIT; diff --git a/packages/db/migrations/0142.undo.create_omnivore_admin_role.sql b/packages/db/migrations/0142.undo.create_omnivore_admin_role.sql new file mode 100755 index 000000000..67f6c3390 --- /dev/null +++ b/packages/db/migrations/0142.undo.create_omnivore_admin_role.sql @@ -0,0 +1,16 @@ +-- Type: UNDO +-- Name: create_omnivore_admin_role +-- Description: Create omnivore_admin role with admin permissions + +BEGIN; + +DROP POLICY user_admin_policy ON omnivore.user; + +REVOKE ALL PRIVILEGES on omnivore.user from omnivore_admin; +REVOKE ALL PRIVILEGES on SCHEMA omnivore from omnivore_admin; + +DROP OWNED BY omnivore_admin; + +DROP ROLE IF EXISTS omnivore_admin; + +COMMIT; diff --git a/packages/db/package.json b/packages/db/package.json index fce36c4af..0af47351b 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -4,6 +4,7 @@ "description": "", "scripts": { "migrate": "ts-node ./migrate.ts", + "test:typecheck": "tsc --noEmit", "generate": "plop" }, "author": "", diff --git a/packages/import-handler/package.json b/packages/import-handler/package.json index 6d23478f2..0da52c021 100644 --- a/packages/import-handler/package.json +++ b/packages/import-handler/package.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "scripts": { "test": "yarn mocha -r ts-node/register --config mocha-config.json", + "test:typecheck": "tsc --noEmit", "lint": "eslint src --ext ts,js,tsx,jsx", "compile": "tsc", "build": "tsc && yarn copy-files", @@ -52,5 +53,8 @@ "unzip-stream": "^0.3.1", "urlsafe-base64": "^1.0.0", "uuid": "^9.0.0" + }, + "volta": { + "extends": "../../package.json" } } diff --git a/packages/inbound-email-handler/package.json b/packages/inbound-email-handler/package.json index b8a2e4f78..0fa33a650 100644 --- a/packages/inbound-email-handler/package.json +++ b/packages/inbound-email-handler/package.json @@ -11,6 +11,7 @@ "keywords": [], "scripts": { "test": "yarn mocha -r ts-node/register --config mocha-config.json", + "test:typecheck": "tsc --noEmit", "lint": "eslint src --ext ts,js,tsx,jsx", "compile": "tsc", "build": "tsc", @@ -42,5 +43,8 @@ "parse-multipart-data": "^1.2.1", "rfc2047": "^4.0.1", "showdown": "^2.1.0" + }, + "volta": { + "extends": "../../package.json" } } diff --git a/packages/pdf-handler/package.json b/packages/pdf-handler/package.json index 09af9e8c3..bd81243e8 100644 --- a/packages/pdf-handler/package.json +++ b/packages/pdf-handler/package.json @@ -11,6 +11,7 @@ "keywords": [], "scripts": { "test": "yarn mocha -r ts-node/register --config mocha-config.json", + "test:typecheck": "tsc --noEmit", "lint": "eslint src --ext ts,js,tsx,jsx", "compile": "tsc", "build": "tsc", @@ -33,5 +34,8 @@ "axios": "^0.27.2", "concurrently": "^7.0.0", "pdfjs-dist": "^2.9.359" + }, + "volta": { + "extends": "../../package.json" } } diff --git a/packages/puppeteer-parse/package.json b/packages/puppeteer-parse/package.json index 306d905fe..db865b9ea 100644 --- a/packages/puppeteer-parse/package.json +++ b/packages/puppeteer-parse/package.json @@ -25,5 +25,8 @@ }, "scripts": { "test": "mocha test/*.js" + }, + "volta": { + "extends": "../../package.json" } } diff --git a/packages/queue-manager/package.json b/packages/queue-manager/package.json index 7c1319a45..28d59d34a 100644 --- a/packages/queue-manager/package.json +++ b/packages/queue-manager/package.json @@ -8,6 +8,7 @@ "license": "Apache-2.0", "scripts": { "test": "yarn mocha -r ts-node/register --config mocha-config.json", + "test:typecheck": "tsc --noEmit", "lint": "eslint src --ext ts,js,tsx,jsx", "compile": "tsc", "build": "tsc", @@ -28,5 +29,8 @@ "axios": "^1.4.0", "dotenv": "^16.0.1", "jsonwebtoken": "^8.5.1" + }, + "volta": { + "extends": "../../package.json" } } diff --git a/packages/readabilityjs/package.json b/packages/readabilityjs/package.json index bcd6dc508..41c440cdc 100644 --- a/packages/readabilityjs/package.json +++ b/packages/readabilityjs/package.json @@ -41,5 +41,8 @@ "dependencies": { "html-entities": "^2.3.2", "parse-srcset": "^1.0.2" + }, + "volta": { + "extends": "../../package.json" } } diff --git a/packages/rss-handler/package.json b/packages/rss-handler/package.json index 0db5d93fd..af647811e 100644 --- a/packages/rss-handler/package.json +++ b/packages/rss-handler/package.json @@ -8,6 +8,7 @@ "license": "Apache-2.0", "scripts": { "test": "yarn mocha -r ts-node/register --config mocha-config.json", + "test:typecheck": "tsc --noEmit", "lint": "eslint src --ext ts,js,tsx,jsx", "compile": "tsc", "build": "tsc", @@ -28,5 +29,8 @@ "dotenv": "^16.0.1", "jsonwebtoken": "^8.5.1", "rss-parser": "^3.13.0" + }, + "volta": { + "extends": "../../package.json" } } diff --git a/packages/rule-handler/package.json b/packages/rule-handler/package.json index b9eadc9c4..5d722480b 100644 --- a/packages/rule-handler/package.json +++ b/packages/rule-handler/package.json @@ -8,6 +8,7 @@ "license": "Apache-2.0", "scripts": { "test": "yarn mocha -r ts-node/register --config mocha-config.json", + "test:typecheck": "tsc --noEmit", "lint": "eslint src --ext ts,js,tsx,jsx", "compile": "tsc", "build": "tsc", @@ -26,5 +27,8 @@ "dotenv": "^16.0.1", "jsonwebtoken": "^8.5.1", "search-query-parser": "^1.6.0" + }, + "volta": { + "extends": "../../package.json" } } diff --git a/packages/text-to-speech/package.json b/packages/text-to-speech/package.json index a1536e0b1..3047e6fd3 100644 --- a/packages/text-to-speech/package.json +++ b/packages/text-to-speech/package.json @@ -11,6 +11,7 @@ "keywords": [], "scripts": { "test": "yarn mocha -r ts-node/register --config mocha-config.json", + "test:typecheck": "tsc --noEmit", "lint": "eslint src --ext ts,js,tsx,jsx", "compile": "tsc", "build": "tsc", @@ -45,5 +46,8 @@ "natural": "^6.2.0", "redis": "^4.3.1", "underscore": "^1.13.4" + }, + "volta": { + "extends": "../../package.json" } } diff --git a/packages/thumbnail-handler/package.json b/packages/thumbnail-handler/package.json index f6ac4e080..707c36ae7 100644 --- a/packages/thumbnail-handler/package.json +++ b/packages/thumbnail-handler/package.json @@ -8,6 +8,7 @@ "license": "Apache-2.0", "scripts": { "test": "yarn mocha -r ts-node/register --config mocha-config.json", + "test:typecheck": "tsc --noEmit", "lint": "eslint src --ext ts,js,tsx,jsx", "compile": "tsc", "build": "tsc", @@ -30,5 +31,8 @@ "jsonwebtoken": "^8.5.1", "linkedom": "^0.14.26", "urlsafe-base64": "^1.0.0" + }, + "volta": { + "extends": "../../package.json" } } diff --git a/packages/web/Dockerfile b/packages/web/Dockerfile index 249bc5259..ff1ce611b 100644 --- a/packages/web/Dockerfile +++ b/packages/web/Dockerfile @@ -12,6 +12,8 @@ ENV NEXT_PUBLIC_BASE_URL=$BASE_URL ENV NEXT_PUBLIC_SERVER_BASE_URL=$SERVER_BASE_URL ENV NEXT_PUBLIC_HIGHLIGHTS_BASE_URL=$HIGHLIGHTS_BASE_URL +RUN apk add g++ make python3 + WORKDIR /app COPY package.json . diff --git a/packages/web/components/elements/Button.tsx b/packages/web/components/elements/Button.tsx index 2939850cf..506fd869b 100644 --- a/packages/web/components/elements/Button.tsx +++ b/packages/web/components/elements/Button.tsx @@ -28,11 +28,9 @@ export const Button = styled('button', { color: '#3D3D3D', bg: '#FFEA9F', p: '10px 15px', - '&:hover': { + '&:hover, &:focus': { bg: '$omnivoreCtaYellow', - }, - '&:focus': { - border: '1px solid $omnivoreCtaYellow', + outline: '1px solid $omnivoreCtaYellow', }, }, cancelGeneric: { @@ -45,18 +43,13 @@ export const Button = styled('button', { border: '1px solid transparent', p: '10px 15px', bg: 'transparent', - '&:hover': { + '&:hover, &:focus': { bg: '#EBEBEB', - }, - '&:focus': { - outline: 'none !important', - border: '1px solid $omnivoreCtaYellow', + outline: '1px solid $omnivoreCtaYellow', }, }, ctaOutlineYellow: { boxSizing: 'border-box', - '-moz-box-sizing': 'border-box', - '-webkit-box-sizing': 'border-box', borderColor: 'unset', border: '1px solid $omnivoreCtaYellow', fontSize: '14px', @@ -159,7 +152,6 @@ export const Button = styled('button', { outlineColor: 'rgba(0, 0, 0, 0)', border: '1px solid rgba(0, 0, 0, 0.06)', cursor: 'pointer', - '&:focus': { outline: 'none' }, }, ctaModal: { height: '32px', @@ -172,7 +164,6 @@ export const Button = styled('button', { border: '1px solid $grayBorder', cursor: 'pointer', borderRadius: '8px', - '&:focus': { outline: 'none' }, }, ctaSecondary: { color: '$grayText', @@ -245,7 +236,6 @@ export const Button = styled('button', { '&:hover': { opacity: 0.7, }, - '&:focus': { outline: 'none' }, }, highlightBarIcon: { p: '0px', @@ -257,7 +247,6 @@ export const Button = styled('button', { opacity: 0.5, }, - '&:focus': { outline: 'none' }, }, articleActionIcon: { bg: 'transparent', diff --git a/packages/web/components/elements/HeaderNavLink.tsx b/packages/web/components/elements/HeaderNavLink.tsx index 149253bb5..0db02fa95 100644 --- a/packages/web/components/elements/HeaderNavLink.tsx +++ b/packages/web/components/elements/HeaderNavLink.tsx @@ -11,37 +11,35 @@ type HeaderNavLinkProps = { export function HeaderNavLink(props: HeaderNavLinkProps): JSX.Element { return ( - - - + + {props.text} + - {props.text} - - - + /> + ) } diff --git a/packages/web/components/elements/LabelColorDropdown.tsx b/packages/web/components/elements/LabelColorDropdown.tsx index 69d7a79e9..3fc4d6fd5 100644 --- a/packages/web/components/elements/LabelColorDropdown.tsx +++ b/packages/web/components/elements/LabelColorDropdown.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react' +import { useRef, useState } from 'react' import { styled } from '../tokens/stitches.config' import { Box, HStack } from './LayoutPrimitives' import { StyledText } from './StyledText' @@ -6,9 +6,14 @@ import { LabelColorDropdownProps, LabelOptionProps, } from '../../utils/settings-page/labels/types' -import { TwitterPicker } from 'react-color' +import { TwitterPicker as TwitterPicker_, TwitterPickerProps } from 'react-color' import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' +// TwitterPicker is a Class component, but the types are broken in React 18. +// TODO: Maybe move away from this component, since it hasn't been updated for 3 years. +// https://github.com/casesandberg/react-color/issues/883 +const TwitterPicker = TwitterPicker_ as unknown as React.FunctionComponent + const DropdownMenuContent = styled(DropdownMenuPrimitive.Content, { borderRadius: 6, backgroundColor: '$grayBg', diff --git a/packages/web/components/elements/LabelsPicker.tsx b/packages/web/components/elements/LabelsPicker.tsx index b234923e6..3aac0a1eb 100644 --- a/packages/web/components/elements/LabelsPicker.tsx +++ b/packages/web/components/elements/LabelsPicker.tsx @@ -1,4 +1,4 @@ -import AutosizeInput from 'react-input-autosize' +import AutosizeInput_, { AutosizeInputProps } from 'react-input-autosize' import { Box, SpanBox } from './LayoutPrimitives' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Label } from '../../lib/networking/fragments/labelFragment' @@ -8,6 +8,11 @@ import { EditLabelChip } from './EditLabelChip' import { LabelsDispatcher } from '../../lib/hooks/useSetPageLabels' import { EditLabelChipStack } from './EditLabelChipStack' +// AutosizeInput is a Class component, but the types are broken in React 18. +// TODO: Maybe move away from this component, since it hasn't been updated for 3 years. +// https://github.com/JedWatson/react-input-autosize/issues +const AutosizeInput = AutosizeInput_ as unknown as React.FunctionComponent + const MaxUnstackedLabels = 7 type LabelsPickerProps = { diff --git a/packages/web/components/elements/SuggestionBox.tsx b/packages/web/components/elements/SuggestionBox.tsx index fd01bf44d..5118708af 100644 --- a/packages/web/components/elements/SuggestionBox.tsx +++ b/packages/web/components/elements/SuggestionBox.tsx @@ -40,14 +40,14 @@ const InternalOrExternalLink = (props: InternalOrExternalLinkProps) => { }} > {!isExternal ? ( - {props.children} + {props.children} ) : ( {props.children} )} - ) + ); } export const SuggestionBox = (props: SuggestionBoxProps) => { diff --git a/packages/web/components/elements/Tooltip.tsx b/packages/web/components/elements/Tooltip.tsx index b1139061a..69ff37a38 100644 --- a/packages/web/components/elements/Tooltip.tsx +++ b/packages/web/components/elements/Tooltip.tsx @@ -53,6 +53,7 @@ export const TooltipContent = StyledContent export const TooltipArrow = StyledArrow type TooltipWrappedProps = { + children: React.ReactNode; tooltipContent: string; active?: boolean; tooltipSide?: TooltipPrimitive.TooltipContentProps['side'] diff --git a/packages/web/components/elements/images/ChromeIcon.tsx b/packages/web/components/elements/images/ChromeIcon.tsx index 2b803e63b..2619c7be6 100644 --- a/packages/web/components/elements/images/ChromeIcon.tsx +++ b/packages/web/components/elements/images/ChromeIcon.tsx @@ -1,5 +1,5 @@ import Image from 'next/image' export function ChromeIcon(): JSX.Element { - return + return } diff --git a/packages/web/components/elements/images/EdgeIcon.tsx b/packages/web/components/elements/images/EdgeIcon.tsx index 783ca59a5..8bee568ed 100644 --- a/packages/web/components/elements/images/EdgeIcon.tsx +++ b/packages/web/components/elements/images/EdgeIcon.tsx @@ -1,5 +1,5 @@ import Image from 'next/image' export function EdgeIcon(): JSX.Element { - return + return } diff --git a/packages/web/components/elements/images/FirefoxIcon.tsx b/packages/web/components/elements/images/FirefoxIcon.tsx index baeb4f83b..5775596d2 100644 --- a/packages/web/components/elements/images/FirefoxIcon.tsx +++ b/packages/web/components/elements/images/FirefoxIcon.tsx @@ -1,5 +1,5 @@ import Image from 'next/image' export function FirefoxIcon(): JSX.Element { - return + return } diff --git a/packages/web/components/elements/images/OmnivoreFestiveLogo.tsx b/packages/web/components/elements/images/OmnivoreFestiveLogo.tsx index aca045d1e..a294b7970 100644 --- a/packages/web/components/elements/images/OmnivoreFestiveLogo.tsx +++ b/packages/web/components/elements/images/OmnivoreFestiveLogo.tsx @@ -14,20 +14,22 @@ export function OmnivoreFestiveLogo( const href = props.href || '/home' return ( - - - - - - ) + ( + + + + ) + ); } diff --git a/packages/web/components/elements/images/OmnivoreLogoBase.tsx b/packages/web/components/elements/images/OmnivoreLogoBase.tsx index ecbce303e..ad827f9a8 100644 --- a/packages/web/components/elements/images/OmnivoreLogoBase.tsx +++ b/packages/web/components/elements/images/OmnivoreLogoBase.tsx @@ -1,8 +1,5 @@ import Link from 'next/link' import { useRouter } from 'next/router' -import { ReactChildren } from 'react' -import { config } from '../../tokens/stitches.config' - export type OmnivoreLogoBaseProps = { color?: string href?: string @@ -15,23 +12,25 @@ export function OmnivoreLogoBase(props: OmnivoreLogoBaseProps): JSX.Element { const router = useRouter() return ( - - { - const query = window.sessionStorage.getItem('q') - if (query) { - router.push(`/home?${query}`) - event.preventDefault() - } - }} - > - {props.children} - + { + const query = window.sessionStorage.getItem('q') + if (query) { + router.push(`/home?${query}`) + event.preventDefault() + } + }} + tabIndex={-1} + aria-label="Omnivore logo" + > + {props.children} ) } diff --git a/packages/web/components/elements/images/SafariIcon.tsx b/packages/web/components/elements/images/SafariIcon.tsx index a8187fc89..e14b598a0 100644 --- a/packages/web/components/elements/images/SafariIcon.tsx +++ b/packages/web/components/elements/images/SafariIcon.tsx @@ -1,5 +1,5 @@ import Image from 'next/image' export function SafariIcon(): JSX.Element { - return + return } diff --git a/packages/web/components/patterns/ArticleNotes.tsx b/packages/web/components/patterns/ArticleNotes.tsx index a4a786b99..5b62ba91e 100644 --- a/packages/web/components/patterns/ArticleNotes.tsx +++ b/packages/web/components/patterns/ArticleNotes.tsx @@ -40,7 +40,7 @@ type NoteSectionProps = { export function ArticleNotes(props: NoteSectionProps): JSX.Element { const saveText = useCallback( - (text) => { + (text: string) => { props.saveText(text) }, [props] @@ -78,7 +78,7 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element { const [lastSaved, setLastSaved] = useState(undefined) const saveText = useCallback( - (text) => { + (text: string) => { ;(async () => { const success = await updateHighlightMutation({ annotation: text, diff --git a/packages/web/components/patterns/HighlightNotes.tsx b/packages/web/components/patterns/HighlightNotes.tsx index 253caa3d3..076354018 100644 --- a/packages/web/components/patterns/HighlightNotes.tsx +++ b/packages/web/components/patterns/HighlightNotes.tsx @@ -48,7 +48,7 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element { const [errorSaving, setErrorSaving] = useState(undefined) const saveText = useCallback( - (text, updateTime, interactive) => { + (text: string, updateTime: Date, interactive: boolean) => { ;(async () => { const success = await updateHighlightMutation({ annotation: text, diff --git a/packages/web/components/patterns/LibraryCards/LibraryHighlightGridCard.tsx b/packages/web/components/patterns/LibraryCards/LibraryHighlightGridCard.tsx index d3ee8a620..0ad21d885 100644 --- a/packages/web/components/patterns/LibraryCards/LibraryHighlightGridCard.tsx +++ b/packages/web/components/patterns/LibraryCards/LibraryHighlightGridCard.tsx @@ -30,10 +30,10 @@ export function LibraryHighlightGridCard( props: LibraryHighlightGridCardProps ): JSX.Element { const [expanded, setExpanded] = useState(false) - const higlightCount = props.item.highlights?.length ?? 0 + const highlightCount = props.item.highlights?.length ?? 0 const router = useRouter() const viewInReader = useCallback( - (highlightId) => { + (highlightId: string) => { if (!router || !router.isReady || !props.viewer) { showErrorToast('Error navigating to highlight') return @@ -197,7 +197,7 @@ export function LibraryHighlightGridCard( event.preventDefault() }} > - {`View ${higlightCount} highlight${higlightCount > 1 ? 's' : ''}`} + {`View ${highlightCount} highlight${highlightCount > 1 ? 's' : ''}`} @@ -49,9 +50,11 @@ export function About(props: AboutProps): JSX.Element { readers.`} diff --git a/packages/web/components/templates/ErrorLayout.tsx b/packages/web/components/templates/ErrorLayout.tsx index b3370b928..be97b9c20 100644 --- a/packages/web/components/templates/ErrorLayout.tsx +++ b/packages/web/components/templates/ErrorLayout.tsx @@ -32,11 +32,11 @@ export function ErrorLayout(props: ErrorLayoutProps): JSX.Element { - + - ) + ); } diff --git a/packages/web/components/templates/LoginForm.tsx b/packages/web/components/templates/LoginForm.tsx index c6f49e661..ed475a012 100644 --- a/packages/web/components/templates/LoginForm.tsx +++ b/packages/web/components/templates/LoginForm.tsx @@ -50,21 +50,21 @@ export function LoginForm(props: LoginFormProps): JSX.Element { > Save articles and read them later in our distraction-free reader. - - - - Learn More -> - - + + + + Learn More -> + + @@ -103,7 +103,7 @@ export function LoginForm(props: LoginFormProps): JSX.Element { /> )} - + - ) + ); } function GoogleAuthButton() { @@ -154,17 +154,17 @@ export function TermAndConditionsFooter(): JSX.Element { }} > By signing up, you agree to Omnivore’s{' '} - + Terms of Service {' '} and{' '} - + Privacy Policy - ) + ); } diff --git a/packages/web/components/templates/LoginLayout.tsx b/packages/web/components/templates/LoginLayout.tsx index 4c6bbe82a..a495047a1 100644 --- a/packages/web/components/templates/LoginLayout.tsx +++ b/packages/web/components/templates/LoginLayout.tsx @@ -1,3 +1,4 @@ +import { unstable_getImgProps as getImgProps } from 'next/image' import { Box, HStack, @@ -9,6 +10,9 @@ import type { LoginFormProps } from './LoginForm' import { OmnivoreNameLogo } from '../elements/images/OmnivoreNameLogo' import { theme } from '../tokens/stitches.config' +import featureFullWidthImage from '../../public/static/images/login/login-feature-image-full.png' +import featureHalfWidthImage from '../../public/static/images/login/login-feature-image-half.png' + export function LoginLayout(props: LoginFormProps): JSX.Element { return ( <> @@ -86,7 +90,30 @@ function MediumLoginLayout(props: LoginFormProps) { ) } +const srcSetToImageSet = (srcFallback: string, srcSet?: string): string => { + if (!srcSet) return `url(${srcFallback})` + + return `image-set( ${srcSet + .split(', ') + .map((subSrc) => { + const [src, resolution] = subSrc.split(' ') + return `url("${decodeURIComponent(src)}") ${resolution}` + }) + .join(',')} +)` +} + function OmnivoreIllustration() { + const { props: halfWidthImgProps } = getImgProps({ + src: featureHalfWidthImage, + alt: '', + }) + + const { props: fullWidthImgProps } = getImgProps({ + src: featureFullWidthImage, + alt: '', + }) + return ( ) diff --git a/packages/web/components/templates/SettingsMenu.tsx b/packages/web/components/templates/SettingsMenu.tsx index 7789f960a..cfe9d60c1 100644 --- a/packages/web/components/templates/SettingsMenu.tsx +++ b/packages/web/components/templates/SettingsMenu.tsx @@ -204,7 +204,7 @@ function SettingsButton(props: SettingsButtonProps): JSX.Element { }, [props, router]) return ( - + - ) + ); } diff --git a/packages/web/components/templates/UploadModal.tsx b/packages/web/components/templates/UploadModal.tsx index ec8b296c1..f6a2c605f 100644 --- a/packages/web/components/templates/UploadModal.tsx +++ b/packages/web/components/templates/UploadModal.tsx @@ -110,7 +110,7 @@ export function UploadModal(props: UploadModalProps): JSX.Element { const dropzoneRef = useRef(null) const openDialog = useCallback( - (event) => { + (event: React.MouseEvent) => { if (dropzoneRef.current) { dropzoneRef.current.open() } diff --git a/packages/web/components/templates/article/Notebook.tsx b/packages/web/components/templates/article/Notebook.tsx index 21ae2abdb..06b3e370e 100644 --- a/packages/web/components/templates/article/Notebook.tsx +++ b/packages/web/components/templates/article/Notebook.tsx @@ -163,7 +163,7 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element { }, [highlights]) const handleSaveNoteText = useCallback( - (text) => { + (text: string) => { const changeTime = new Date() setLastChanged(changeTime) diff --git a/packages/web/components/templates/article/NotebookModal.tsx b/packages/web/components/templates/article/NotebookModal.tsx index 55205f6be..aefbe20ba 100644 --- a/packages/web/components/templates/article/NotebookModal.tsx +++ b/packages/web/components/templates/article/NotebookModal.tsx @@ -49,7 +49,7 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element { props.onClose(allAnnotations ?? [], deletedHighlights ?? []) }, [props, allAnnotations]) - const handleAnnotationsChange = useCallback((allAnnotations) => { + const handleAnnotationsChange = useCallback((allAnnotations: Highlight[]) => { setAllAnnotations(allAnnotations) }, []) @@ -66,7 +66,7 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element { }, [allAnnotations]) const viewInReader = useCallback( - (highlightId) => { + (highlightId: string) => { props.viewHighlightInReader(highlightId) handleClose() }, diff --git a/packages/web/components/templates/article/ReaderSettingsControl.tsx b/packages/web/components/templates/article/ReaderSettingsControl.tsx index 322fa2194..cc6c034a3 100644 --- a/packages/web/components/templates/article/ReaderSettingsControl.tsx +++ b/packages/web/components/templates/article/ReaderSettingsControl.tsx @@ -292,7 +292,7 @@ function FontControls(props: FontControlsProps): JSX.Element { }) const handleFontSizeChange = useCallback( - (value) => { + (value: number) => { readerSettings.actionHandler('setFontSize', value) }, [readerSettings] @@ -399,7 +399,7 @@ function LayoutControls(props: LayoutControlsProps): JSX.Element { const { readerSettings } = props const handleMarginWidthChange = useCallback( - (value) => { + (value: number) => { readerSettings.setMarginWidth(value) }, [readerSettings] diff --git a/packages/web/components/templates/auth/EmailLogin.tsx b/packages/web/components/templates/auth/EmailLogin.tsx index a7c31950b..62fc4847d 100644 --- a/packages/web/components/templates/auth/EmailLogin.tsx +++ b/packages/web/components/templates/auth/EmailLogin.tsx @@ -122,7 +122,7 @@ export function EmailLogin(): JSX.Element { }} > Don't have an account?{' '} - + Sign up @@ -140,7 +140,7 @@ export function EmailLogin(): JSX.Element { }} > Forgot your password?{' '} - + Click here @@ -148,5 +148,5 @@ export function EmailLogin(): JSX.Element { - ) + ); } diff --git a/packages/web/components/templates/auth/EmailSignup.tsx b/packages/web/components/templates/auth/EmailSignup.tsx index d3becc8ec..e23a5f584 100644 --- a/packages/web/components/templates/auth/EmailSignup.tsx +++ b/packages/web/components/templates/auth/EmailSignup.tsx @@ -206,7 +206,7 @@ export function EmailSignup(): JSX.Element { }} > Already have an account?{' '} - + Login instead @@ -215,5 +215,5 @@ export function EmailSignup(): JSX.Element { - ) + ); } diff --git a/packages/web/components/templates/homeFeed/HighlightItem.tsx b/packages/web/components/templates/homeFeed/HighlightItem.tsx index 4aa97911e..b98f8a7c0 100644 --- a/packages/web/components/templates/homeFeed/HighlightItem.tsx +++ b/packages/web/components/templates/homeFeed/HighlightItem.tsx @@ -108,7 +108,7 @@ export function HighlightsMenu(props: HighlightsMenuProps): JSX.Element { + legacyBehavior> { console.log('event.ctrlKey: ', event.ctrlKey, event.metaKey) @@ -129,7 +129,7 @@ export function HighlightsMenu(props: HighlightsMenuProps): JSX.Element { - ) + ); } const sortHighlights = (highlights: Highlight[]) => { diff --git a/packages/web/components/templates/homeFeed/HighlightsLayout.tsx b/packages/web/components/templates/homeFeed/HighlightsLayout.tsx index d0260dd75..75ffcf5d0 100644 --- a/packages/web/components/templates/homeFeed/HighlightsLayout.tsx +++ b/packages/web/components/templates/homeFeed/HighlightsLayout.tsx @@ -368,7 +368,7 @@ function HighlightList(props: HighlightListProps): JSX.Element { }, [props.item.node.highlights]) const viewInReader = useCallback( - (highlightId) => { + (highlightId: string) => { if (!router || !router.isReady || !props.viewer) { showErrorToast('Error navigating to highlight') return diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index fa8476091..45dd10655 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -351,7 +351,7 @@ export function HomeFeedContainer(): JSX.Element { }, [libraryItems, activeCardId]) const getItem = useCallback( - (itemId) => { + (itemId: string) => { return libraryItems.find((item) => item.node.id === itemId) }, [libraryItems] diff --git a/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx index fba2015c9..6fb5a6a2f 100644 --- a/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx +++ b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx @@ -553,7 +553,7 @@ type EditButtonProps = { function EditButton(props: EditButtonProps): JSX.Element { return ( - + - ) + ); } diff --git a/packages/web/components/templates/integrations/Webhooks.tsx b/packages/web/components/templates/integrations/Webhooks.tsx index d36ab8a64..ce8bcd95e 100644 --- a/packages/web/components/templates/integrations/Webhooks.tsx +++ b/packages/web/components/templates/integrations/Webhooks.tsx @@ -140,7 +140,7 @@ export function Webhooks(): JSX.Element { }} >

{item.method}

-

{item.createdAt}

+

{item.createdAt?.toLocaleDateString()}

) diff --git a/packages/web/components/templates/landing/LandingFooter.tsx b/packages/web/components/templates/landing/LandingFooter.tsx index 71920855f..66e682148 100644 --- a/packages/web/components/templates/landing/LandingFooter.tsx +++ b/packages/web/components/templates/landing/LandingFooter.tsx @@ -1,4 +1,4 @@ -import { HStack, VStack } from '../../elements/LayoutPrimitives' +import { Box, HStack, VStack } from '../../elements/LayoutPrimitives' import { styled } from '../../tokens/stitches.config' import { StyledText } from '../../elements/StyledText' @@ -30,11 +30,29 @@ export function LandingFooter(): JSX.Element { textDecoration: 'underline', }, }, + '@mdDown': { + columns: 2, + width: '100%', + marginTop: "8px", + marginBottom: "30px", + }, }) return ( - + Install @@ -77,7 +95,7 @@ export function LandingFooter(): JSX.Element {
  • - Contact us via email + Contact us via email
  • @@ -112,7 +130,7 @@ export function LandingFooter(): JSX.Element {
  • -
    +
    ) } diff --git a/packages/web/components/templates/landing/LandingHeader.tsx b/packages/web/components/templates/landing/LandingHeader.tsx index 2d30cde14..5f91bf821 100644 --- a/packages/web/components/templates/landing/LandingHeader.tsx +++ b/packages/web/components/templates/landing/LandingHeader.tsx @@ -1,16 +1,18 @@ import { Box } from '../../elements/LayoutPrimitives' import { OmnivoreNameLogo } from '../../elements/images/OmnivoreNameLogo' import { Button } from '../../elements/Button' +import Link from 'next/link' const LoginButton = (): JSX.Element => { return ( - ) : ( - - )} - - - Don't have an Omnivore account?{' '} - - - Signup - - - - -
    - - - ) + + {viewerData?.me ? ( + + ) : ( + + )} + + + Don't have an Omnivore account?{' '} + + + Signup + + + + +
    + + ; } diff --git a/packages/web/pages/settings/account.tsx b/packages/web/pages/settings/account.tsx index 0051bbbd2..d4cc1252a 100644 --- a/packages/web/pages/settings/account.tsx +++ b/packages/web/pages/settings/account.tsx @@ -8,7 +8,7 @@ import { } from '../../components/elements/LayoutPrimitives' import { StyledText } from '../../components/elements/StyledText' import { SettingsLayout } from '../../components/templates/SettingsLayout' -import { styled } from '../../components/tokens/stitches.config' +import { styled, theme } from '../../components/tokens/stitches.config' import { updateEmailMutation } from '../../lib/networking/mutations/updateEmailMutation' import { updateUserMutation } from '../../lib/networking/mutations/updateUserMutation' import { updateUserProfileMutation } from '../../lib/networking/mutations/updateUserProfileMutation' @@ -18,6 +18,9 @@ import { useValidateUsernameQuery } from '../../lib/networking/queries/useValida import { applyStoredTheme } from '../../lib/themeUpdater' import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers' import { ConfirmationModal } from '../../components/patterns/ConfirmationModal' +import { ProgressBar } from '../../components/elements/ProgressBar' + +const ACCOUNT_LIMIT = 50_000 const StyledLabel = styled('label', { fontWeight: 600, @@ -371,7 +374,7 @@ export default function Account(): JSX.Element { - {/* - - {`${libraryCount} of 50K library items used.`} + + {`${libraryCount} of ${ACCOUNT_LIMIT} library items used.`} + + + NOTE: this is a soft limit, if you are approaching or have + exceeded this limit please contact support to have your limit + raised. )} - - */} + {/* */} + - Deleting your account will delete all your saved items, notes, and - highlights. This operation can not be undone. +

    + Deleting your account will delete all your saved items, notes, and + highlights. This operation can not be undone. Note that once your + account is deleted you will not be able to create a new account + with the same username/email for at least 24hrs. +

    +

    + If you are deleting your account because you've imported too + many items, and want a "fresh start" you can try using + the bulk tool to clean up your + account instead. +

    {viewer && router ? (