Merge pull request #3952 from omnivore-app/fix/ios-digest-new-users
Allow opt into digest on the library view
This commit is contained in:
File diff suppressed because one or more lines are too long
@ -396,7 +396,7 @@ public struct ShareExtensionView: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
.font(Font.system(size: 14))
|
||||
.accentColor(.blue)
|
||||
#if os(macos)
|
||||
#if os(macOS)
|
||||
.introspectTextView { textView in
|
||||
textView.textContainerInset = NSSize(width: 10, height: 10)
|
||||
}
|
||||
|
||||
@ -9,6 +9,10 @@ import Transmission
|
||||
@MainActor
|
||||
public class DigestConfigViewModel: ObservableObject {
|
||||
@Published var isLoading = false
|
||||
@Published var digestEnabled = false
|
||||
|
||||
@Published var isIneligible = false
|
||||
@Published var hasOptInError = false
|
||||
@Published var digest: DigestResult?
|
||||
@Published var chapterInfo: [(DigestChapter, DigestChapterData)]?
|
||||
@Published var presentedLibraryItem: String?
|
||||
@ -16,23 +20,30 @@ public class DigestConfigViewModel: ObservableObject {
|
||||
|
||||
@AppStorage(UserDefaultKey.lastVisitedDigestId.rawValue) var lastVisitedDigestId = ""
|
||||
|
||||
func load(dataService: DataService) async {
|
||||
func checkAlreadyOptedIn(dataService: DataService) async {
|
||||
isLoading = true
|
||||
if !dataService.digestNeedsRefresh() {
|
||||
if let digest = dataService.loadStoredDigest() {
|
||||
self.digest = digest
|
||||
if let user = try? await dataService.fetchViewer() {
|
||||
digestEnabled = user.hasFeatureGranted("ai-digest")
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
func enableDigest(dataService: DataService) async {
|
||||
isLoading = true
|
||||
do {
|
||||
if try await dataService.optInFeature(name: "ai-digest") == nil {
|
||||
throw BasicError.message(messageText: "Could not opt into feature")
|
||||
}
|
||||
} else {
|
||||
do {
|
||||
if let digest = try await dataService.getLatestDigest(timeoutInterval: 10) {
|
||||
self.digest = digest
|
||||
}
|
||||
} catch {
|
||||
print("ERROR WITH DIGEST: ", error)
|
||||
self.digest = nil
|
||||
try await dataService.setupUserDigestConfig()
|
||||
try await dataService.refreshDigest()
|
||||
digestEnabled = true
|
||||
} catch {
|
||||
if error is IneligibleError {
|
||||
isIneligible = true
|
||||
} else {
|
||||
hasOptInError = true
|
||||
}
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
@ -41,12 +52,14 @@ public class DigestConfigViewModel: ObservableObject {
|
||||
@MainActor
|
||||
struct DigestConfigView: View {
|
||||
@StateObject var viewModel = DigestConfigViewModel()
|
||||
let homeViewModel: HomeFeedViewModel
|
||||
let dataService: DataService
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
public init(dataService: DataService) {
|
||||
public init(dataService: DataService, homeViewModel: HomeFeedViewModel) {
|
||||
self.dataService = dataService
|
||||
self.homeViewModel = homeViewModel
|
||||
}
|
||||
|
||||
var titleBlock: some View {
|
||||
@ -65,12 +78,31 @@ struct DigestConfigView: View {
|
||||
VStack {
|
||||
titleBlock
|
||||
.padding(.top, 10)
|
||||
itemBody
|
||||
.padding(15)
|
||||
|
||||
if viewModel.isLoading {
|
||||
HStack {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 50)
|
||||
} else if viewModel.digestEnabled {
|
||||
Text("You've been added to the AI Digest demo. You first issue should be ready soon.")
|
||||
.padding(15)
|
||||
} else if viewModel.isIneligible {
|
||||
Text("To enable digest you need to have saved at least ten library items and have two active subscriptions.")
|
||||
.padding(15)
|
||||
} else if viewModel.hasOptInError {
|
||||
Text("There was an error setting up digest for your account.")
|
||||
.padding(15)
|
||||
} else {
|
||||
itemBody
|
||||
.padding(15)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}.task {
|
||||
await viewModel.load(dataService: dataService)
|
||||
await viewModel.checkAlreadyOptedIn(dataService: dataService)
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,10 +155,17 @@ struct DigestConfigView: View {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
Button(action: {}, label: { Text("Hide digest") })
|
||||
Button(action: {
|
||||
homeViewModel.hideDigestIcon = true
|
||||
dismiss()
|
||||
}, label: { Text("Hide digest") })
|
||||
.buttonStyle(RoundedRectButtonStyle())
|
||||
|
||||
Button(action: {}, label: { Text("Enable digest") })
|
||||
Button(action: {
|
||||
Task {
|
||||
await viewModel.enableDigest(dataService: dataService)
|
||||
}
|
||||
}, label: { Text("Enable digest") })
|
||||
.buttonStyle(RoundedRectButtonStyle(color: Color.blue, textColor: Color.white))
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,6 +38,7 @@ func formatTimeInterval(_ time: TimeInterval) -> String? {
|
||||
@MainActor
|
||||
public class FullScreenDigestViewModel: ObservableObject {
|
||||
@Published var isLoading = false
|
||||
@Published var hasError = false
|
||||
@Published var digest: DigestResult?
|
||||
@Published var chapterInfo: [(DigestChapter, DigestChapterData)]?
|
||||
@Published var presentedLibraryItem: String?
|
||||
@ -46,6 +47,9 @@ public class FullScreenDigestViewModel: ObservableObject {
|
||||
@AppStorage(UserDefaultKey.lastVisitedDigestId.rawValue) var lastVisitedDigestId = ""
|
||||
|
||||
func load(dataService: DataService, audioController: AudioController) async {
|
||||
hasError = false
|
||||
isLoading = true
|
||||
|
||||
if !dataService.digestNeedsRefresh() {
|
||||
if let digest = dataService.loadStoredDigest() {
|
||||
self.digest = digest
|
||||
@ -72,6 +76,8 @@ public class FullScreenDigestViewModel: ObservableObject {
|
||||
let chapterData = self.chapterInfo?.map { $0.1 }
|
||||
audioController.play(itemAudioProperties: DigestAudioItem(digest: digest, chapters: chapterData ?? []))
|
||||
}
|
||||
} else {
|
||||
hasError = true
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
@ -164,6 +170,19 @@ struct FullScreenDigestView: View {
|
||||
ProgressView()
|
||||
Spacer()
|
||||
}
|
||||
} else if viewModel.hasError {
|
||||
VStack {
|
||||
Spacer()
|
||||
Text("There was an error loading your digest.")
|
||||
Button(action: {
|
||||
Task {
|
||||
await viewModel.load(dataService: dataService, audioController: audioController)
|
||||
}
|
||||
}, label: { Text("Try again") })
|
||||
.buttonStyle(RoundedRectButtonStyle(color: Color.blue, textColor: Color.white))
|
||||
|
||||
Spacer()
|
||||
}
|
||||
} else {
|
||||
itemBody
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// swiftlint:disable file_length type_body_length
|
||||
import CoreData
|
||||
import Models
|
||||
import Services
|
||||
@ -334,7 +335,7 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
.sheet(isPresented: $showDigestConfig) {
|
||||
if #available(iOS 17.0, *) {
|
||||
NavigationView {
|
||||
DigestConfigView(dataService: dataService)
|
||||
DigestConfigView(dataService: dataService, homeViewModel: viewModel)
|
||||
}
|
||||
} else {
|
||||
Text("Sorry digest is only available on iOS 17 and above")
|
||||
@ -415,17 +416,7 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
)
|
||||
.buttonStyle(.plain)
|
||||
.padding(.trailing, 4)
|
||||
}
|
||||
// if #available(iOS 17.0, *), !dataService.featureFlags.digestEnabled, !viewModel.digestHidden {
|
||||
// Button(
|
||||
// action: { showDigestConfig = true },
|
||||
// label: { Image.tabDigestSelected }
|
||||
// )
|
||||
// .buttonStyle(.plain)
|
||||
// .padding(.trailing, 4)
|
||||
// }
|
||||
if #available(iOS 17.0, *), !dataService.featureFlags.digestEnabled, !viewModel.digestHidden {
|
||||
// Give the user an opportunity to enable digest
|
||||
} else if #available(iOS 17.0, *), !dataService.featureFlags.digestEnabled, !viewModel.hideDigestIcon {
|
||||
Button(
|
||||
action: { showDigestConfig = true },
|
||||
label: { Image.tabDigestSelected }
|
||||
|
||||
@ -49,7 +49,7 @@ enum LoadingBarStyle {
|
||||
@AppStorage(UserDefaultKey.hideFeatureSection.rawValue) var hideFeatureSection = false
|
||||
@AppStorage(UserDefaultKey.stopUsingFollowingPrimer.rawValue) var stopUsingFollowingPrimer = false
|
||||
@AppStorage("LibraryTabView::hideFollowingTab") var hideFollowingTab = false
|
||||
@AppStorage("LibraryTabView::digestHidden") var digestHidden = false
|
||||
@AppStorage("LibraryTabView::hideDigestIcon") var hideDigestIcon = false
|
||||
@AppStorage(UserDefaultKey.lastVisitedDigestId.rawValue) var lastVisitedDigestId = ""
|
||||
|
||||
@AppStorage(UserDefaultKey.lastSelectedFeaturedItemFilter.rawValue) var featureFilter =
|
||||
@ -403,7 +403,8 @@ enum LoadingBarStyle {
|
||||
|
||||
func checkForDigestUpdate(dataService: DataService) async {
|
||||
do {
|
||||
if dataService.featureFlags.digestEnabled, let result = try? await dataService.getLatestDigest(timeoutInterval: 2) {
|
||||
if dataService.featureFlags.digestEnabled,
|
||||
let result = try? await dataService.getLatestDigest(timeoutInterval: 2) {
|
||||
if result.id != lastVisitedDigestId {
|
||||
digestIsUnread = true
|
||||
}
|
||||
|
||||
@ -19,6 +19,8 @@ import Views
|
||||
}
|
||||
|
||||
@AppStorage("LibraryTabView::hideFollowingTab") var hideFollowingTab = false
|
||||
@AppStorage("LibraryTabView::hideDigestIcon") var hideDigestIcon = false
|
||||
|
||||
@AppStorage(UserDefaultKey.hideFeatureSection.rawValue) var hideFeatureSection = false
|
||||
|
||||
func loadFilters(dataService: DataService) async {
|
||||
@ -95,6 +97,7 @@ struct FiltersView: View {
|
||||
List {
|
||||
Section(header: Text("User Interface")) {
|
||||
Toggle("Hide following tab", isOn: $viewModel.hideFollowingTab)
|
||||
Toggle("Hide digest icon", isOn: $viewModel.hideDigestIcon)
|
||||
Toggle("Hide feature section", isOn: $viewModel.hideFeatureSection)
|
||||
}
|
||||
|
||||
|
||||
@ -97,8 +97,7 @@ public struct ExplainResult: Codable {
|
||||
public let text: String
|
||||
}
|
||||
|
||||
extension DataService {
|
||||
|
||||
extension DataService {
|
||||
public func digestNeedsRefresh() -> Bool {
|
||||
let fileManager = FileManager.default
|
||||
let localURL = URL.om_cachesDirectory.appendingPathComponent("digest.json")
|
||||
|
||||
@ -5866,6 +5866,68 @@ extension Selection where TypeLock == Never, Type == Never {
|
||||
typealias DeviceTokensSuccess<T> = Selection<T, Objects.DeviceTokensSuccess>
|
||||
}
|
||||
|
||||
extension Objects {
|
||||
struct DigestConfig {
|
||||
let __typename: TypeName = .digestConfig
|
||||
let channels: [String: [String?]]
|
||||
|
||||
enum TypeName: String, Codable {
|
||||
case digestConfig = "DigestConfig"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Objects.DigestConfig: 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 "channels":
|
||||
if let value = try container.decode([String?]?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
default:
|
||||
throw DecodingError.dataCorrupted(
|
||||
DecodingError.Context(
|
||||
codingPath: decoder.codingPath,
|
||||
debugDescription: "Unknown key \(field)."
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
channels = map["channels"]
|
||||
}
|
||||
}
|
||||
|
||||
extension Fields where TypeLock == Objects.DigestConfig {
|
||||
func channels() throws -> [String?]? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "channels",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return data.channels[field.alias!]
|
||||
case .mocking:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Selection where TypeLock == Never, Type == Never {
|
||||
typealias DigestConfig<T> = Selection<T, Objects.DigestConfig>
|
||||
}
|
||||
|
||||
extension Objects {
|
||||
struct DiscoverFeed {
|
||||
let __typename: TypeName = .discoverFeed
|
||||
@ -28003,7 +28065,7 @@ extension Selection where TypeLock == Never, Type == Never {
|
||||
extension Objects {
|
||||
struct UserPersonalization {
|
||||
let __typename: TypeName = .userPersonalization
|
||||
let digestConfig: [String: String]
|
||||
let digestConfig: [String: Objects.DigestConfig]
|
||||
let fields: [String: String]
|
||||
let fontFamily: [String: String]
|
||||
let fontSize: [String: Int]
|
||||
@ -28036,7 +28098,7 @@ extension Objects.UserPersonalization: Decodable {
|
||||
|
||||
switch field {
|
||||
case "digestConfig":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
if let value = try container.decode(Objects.DigestConfig?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "fields":
|
||||
@ -28114,18 +28176,19 @@ extension Objects.UserPersonalization: Decodable {
|
||||
}
|
||||
|
||||
extension Fields where TypeLock == Objects.UserPersonalization {
|
||||
func digestConfig() throws -> String? {
|
||||
let field = GraphQLField.leaf(
|
||||
func digestConfig<Type>(selection: Selection<Type, Objects.DigestConfig?>) throws -> Type {
|
||||
let field = GraphQLField.composite(
|
||||
name: "digestConfig",
|
||||
arguments: []
|
||||
arguments: [],
|
||||
selection: selection.selection
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return data.digestConfig[field.alias!]
|
||||
return try selection.decode(data: data.digestConfig[field.alias!])
|
||||
case .mocking:
|
||||
return nil
|
||||
return selection.mock()
|
||||
}
|
||||
}
|
||||
|
||||
@ -39401,6 +39464,21 @@ extension InputObjects {
|
||||
}
|
||||
}
|
||||
|
||||
extension InputObjects {
|
||||
struct DigestConfigInput: Encodable, Hashable {
|
||||
var channels: OptionalArgument<[OptionalArgument<String>]> = .absent()
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
if channels.hasValue { try container.encode(channels, forKey: .channels) }
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case channels
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension InputObjects {
|
||||
struct EditDiscoverFeedInput: Encodable, Hashable {
|
||||
var feedId: String
|
||||
@ -40379,7 +40457,7 @@ extension InputObjects {
|
||||
|
||||
extension InputObjects {
|
||||
struct SetUserPersonalizationInput: Encodable, Hashable {
|
||||
var digestConfig: OptionalArgument<String> = .absent()
|
||||
var digestConfig: OptionalArgument<InputObjects.DigestConfigInput> = .absent()
|
||||
|
||||
var fields: OptionalArgument<String> = .absent()
|
||||
|
||||
|
||||
@ -1,10 +1,3 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Jackson Harper on 11/10/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Models
|
||||
import SwiftGraphQL
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -40,7 +40,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct LibraryItemEntity: AppEntity {
|
||||
static var defaultQuery = LibraryItemQuery()
|
||||
|
||||
@ -805,6 +805,15 @@ export type DeviceTokensSuccess = {
|
||||
deviceTokens: Array<DeviceToken>;
|
||||
};
|
||||
|
||||
export type DigestConfig = {
|
||||
__typename?: 'DigestConfig';
|
||||
channels?: Maybe<Array<Maybe<Scalars['String']>>>;
|
||||
};
|
||||
|
||||
export type DigestConfigInput = {
|
||||
channels?: InputMaybe<Array<InputMaybe<Scalars['String']>>>;
|
||||
};
|
||||
|
||||
export enum DirectionalityType {
|
||||
Ltr = 'LTR',
|
||||
Rtl = 'RTL'
|
||||
@ -3055,7 +3064,7 @@ export enum SetUserPersonalizationErrorCode {
|
||||
}
|
||||
|
||||
export type SetUserPersonalizationInput = {
|
||||
digestConfig?: InputMaybe<Scalars['JSON']>;
|
||||
digestConfig?: InputMaybe<DigestConfigInput>;
|
||||
fields?: InputMaybe<Scalars['JSON']>;
|
||||
fontFamily?: InputMaybe<Scalars['String']>;
|
||||
fontSize?: InputMaybe<Scalars['Int']>;
|
||||
@ -3769,7 +3778,7 @@ export enum UserErrorCode {
|
||||
|
||||
export type UserPersonalization = {
|
||||
__typename?: 'UserPersonalization';
|
||||
digestConfig?: Maybe<Scalars['JSON']>;
|
||||
digestConfig?: Maybe<DigestConfig>;
|
||||
fields?: Maybe<Scalars['JSON']>;
|
||||
fontFamily?: Maybe<Scalars['String']>;
|
||||
fontSize?: Maybe<Scalars['Int']>;
|
||||
@ -4084,6 +4093,8 @@ export type ResolversTypes = {
|
||||
DeviceTokensErrorCode: DeviceTokensErrorCode;
|
||||
DeviceTokensResult: ResolversTypes['DeviceTokensError'] | ResolversTypes['DeviceTokensSuccess'];
|
||||
DeviceTokensSuccess: ResolverTypeWrapper<DeviceTokensSuccess>;
|
||||
DigestConfig: ResolverTypeWrapper<DigestConfig>;
|
||||
DigestConfigInput: DigestConfigInput;
|
||||
DirectionalityType: DirectionalityType;
|
||||
DiscoverFeed: ResolverTypeWrapper<DiscoverFeed>;
|
||||
DiscoverFeedArticle: ResolverTypeWrapper<DiscoverFeedArticle>;
|
||||
@ -4648,6 +4659,8 @@ export type ResolversParentTypes = {
|
||||
DeviceTokensError: DeviceTokensError;
|
||||
DeviceTokensResult: ResolversParentTypes['DeviceTokensError'] | ResolversParentTypes['DeviceTokensSuccess'];
|
||||
DeviceTokensSuccess: DeviceTokensSuccess;
|
||||
DigestConfig: DigestConfig;
|
||||
DigestConfigInput: DigestConfigInput;
|
||||
DiscoverFeed: DiscoverFeed;
|
||||
DiscoverFeedArticle: DiscoverFeedArticle;
|
||||
DiscoverFeedError: DiscoverFeedError;
|
||||
@ -5531,6 +5544,11 @@ export type DeviceTokensSuccessResolvers<ContextType = ResolverContext, ParentTy
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type DigestConfigResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['DigestConfig'] = ResolversParentTypes['DigestConfig']> = {
|
||||
channels?: Resolver<Maybe<Array<Maybe<ResolversTypes['String']>>>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type DiscoverFeedResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['DiscoverFeed'] = ResolversParentTypes['DiscoverFeed']> = {
|
||||
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
@ -7233,7 +7251,7 @@ export type UserErrorResolvers<ContextType = ResolverContext, ParentType extends
|
||||
};
|
||||
|
||||
export type UserPersonalizationResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UserPersonalization'] = ResolversParentTypes['UserPersonalization']> = {
|
||||
digestConfig?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
|
||||
digestConfig?: Resolver<Maybe<ResolversTypes['DigestConfig']>, ParentType, ContextType>;
|
||||
fields?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
|
||||
fontFamily?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
fontSize?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
|
||||
@ -7412,6 +7430,7 @@ export type Resolvers<ContextType = ResolverContext> = {
|
||||
DeviceTokensError?: DeviceTokensErrorResolvers<ContextType>;
|
||||
DeviceTokensResult?: DeviceTokensResultResolvers<ContextType>;
|
||||
DeviceTokensSuccess?: DeviceTokensSuccessResolvers<ContextType>;
|
||||
DigestConfig?: DigestConfigResolvers<ContextType>;
|
||||
DiscoverFeed?: DiscoverFeedResolvers<ContextType>;
|
||||
DiscoverFeedArticle?: DiscoverFeedArticleResolvers<ContextType>;
|
||||
DiscoverFeedError?: DiscoverFeedErrorResolvers<ContextType>;
|
||||
|
||||
@ -716,6 +716,14 @@ type DeviceTokensSuccess {
|
||||
deviceTokens: [DeviceToken!]!
|
||||
}
|
||||
|
||||
type DigestConfig {
|
||||
channels: [String]
|
||||
}
|
||||
|
||||
input DigestConfigInput {
|
||||
channels: [String]
|
||||
}
|
||||
|
||||
enum DirectionalityType {
|
||||
LTR
|
||||
RTL
|
||||
@ -2386,7 +2394,7 @@ enum SetUserPersonalizationErrorCode {
|
||||
}
|
||||
|
||||
input SetUserPersonalizationInput {
|
||||
digestConfig: JSON
|
||||
digestConfig: DigestConfigInput
|
||||
fields: JSON
|
||||
fontFamily: String
|
||||
fontSize: Int
|
||||
@ -3046,7 +3054,7 @@ enum UserErrorCode {
|
||||
}
|
||||
|
||||
type UserPersonalization {
|
||||
digestConfig: JSON
|
||||
digestConfig: DigestConfig
|
||||
fields: JSON
|
||||
fontFamily: String
|
||||
fontSize: Int
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
MutationSetUserPersonalizationArgs,
|
||||
SetUserPersonalizationError,
|
||||
SetUserPersonalizationErrorCode,
|
||||
SetUserPersonalizationInput,
|
||||
SetUserPersonalizationSuccess,
|
||||
SortOrder,
|
||||
} from '../../generated/graphql'
|
||||
@ -15,11 +16,20 @@ export const setUserPersonalizationResolver = authorized<
|
||||
SetUserPersonalizationError,
|
||||
MutationSetUserPersonalizationArgs
|
||||
>(async (_, { input }, { authTrx, uid }) => {
|
||||
const newValues = input as Omit<SetUserPersonalizationInput, 'digestConfig'>
|
||||
const digestValues = input.digestConfig
|
||||
? {
|
||||
digestConfig: () => {
|
||||
return JSON.stringify(input.digestConfig)
|
||||
},
|
||||
}
|
||||
: {}
|
||||
const result = await authTrx(async (t) => {
|
||||
return t.getRepository(UserPersonalization).upsert(
|
||||
{
|
||||
user: { id: uid },
|
||||
...input,
|
||||
...newValues,
|
||||
...digestValues,
|
||||
},
|
||||
['user']
|
||||
)
|
||||
|
||||
@ -1067,6 +1067,14 @@ const schema = gql`
|
||||
following: [User!]!
|
||||
}
|
||||
|
||||
type DigestConfig {
|
||||
channels: [String]
|
||||
}
|
||||
|
||||
input DigestConfigInput {
|
||||
channels: [String]
|
||||
}
|
||||
|
||||
type UserPersonalization {
|
||||
id: ID
|
||||
theme: String
|
||||
@ -1080,7 +1088,7 @@ const schema = gql`
|
||||
speechRate: String
|
||||
speechVolume: String
|
||||
fields: JSON
|
||||
digestConfig: JSON
|
||||
digestConfig: DigestConfig
|
||||
}
|
||||
|
||||
# Query: UserPersonalization
|
||||
@ -1123,7 +1131,7 @@ const schema = gql`
|
||||
speechRate: String
|
||||
speechVolume: String
|
||||
fields: JSON
|
||||
digestConfig: JSON
|
||||
digestConfig: DigestConfigInput
|
||||
}
|
||||
|
||||
# Type: ArticleSavingRequest
|
||||
|
||||
@ -102,6 +102,43 @@ describe('User Personalization API', () => {
|
||||
)
|
||||
expect(updatedUserPersonalization?.fields).to.eql(newFields)
|
||||
})
|
||||
|
||||
it('updates and can clear the user personalization', async () => {
|
||||
const newFields = {
|
||||
channels: ['push', 'email'],
|
||||
}
|
||||
|
||||
const res = await graphqlRequest(query, authToken, {
|
||||
input: { fields: newFields },
|
||||
}).expect(200)
|
||||
|
||||
expect(
|
||||
res.body.data.setUserPersonalization.updatedUserPersonalization.fields
|
||||
).to.eql(newFields)
|
||||
|
||||
const updatedUserPersonalization = await findUserPersonalization(
|
||||
user.id
|
||||
)
|
||||
expect(updatedUserPersonalization?.fields).to.eql(newFields)
|
||||
|
||||
const updatedFields = {
|
||||
channels: ['push', 'email'],
|
||||
}
|
||||
|
||||
const updatedRes = await graphqlRequest(query, authToken, {
|
||||
input: { fields: updatedFields },
|
||||
}).expect(200)
|
||||
|
||||
expect(
|
||||
updatedRes.body.data.setUserPersonalization.updatedUserPersonalization
|
||||
.fields
|
||||
).to.eql(updatedFields)
|
||||
|
||||
const updatedUserPersonalization2 = await findUserPersonalization(
|
||||
user.id
|
||||
)
|
||||
expect(updatedUserPersonalization2?.fields).to.eql(newFields)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -27,7 +27,9 @@ export async function updateDigestConfigMutation(
|
||||
}
|
||||
... on SetUserPersonalizationSuccess {
|
||||
updatedUserPersonalization {
|
||||
digestConfig
|
||||
digestConfig {
|
||||
channels
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,7 +58,9 @@ export function useGetUserPersonalization(): UserPersonalizationResult {
|
||||
getUserPersonalization {
|
||||
... on GetUserPersonalizationSuccess {
|
||||
userPersonalization {
|
||||
digestConfig
|
||||
digestConfig {
|
||||
channels
|
||||
}
|
||||
}
|
||||
}
|
||||
... on GetUserPersonalizationError {
|
||||
|
||||
69
yarn.lock
69
yarn.lock
@ -12870,6 +12870,11 @@ cookie-signature@1.0.6:
|
||||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
||||
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
|
||||
|
||||
cookie@0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
|
||||
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
|
||||
|
||||
cookie@0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
|
||||
@ -13170,6 +13175,15 @@ crypto@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037"
|
||||
integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==
|
||||
|
||||
csrf@3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/csrf/-/csrf-3.1.0.tgz#ec75e9656d004d674b8ef5ba47b41fbfd6cb9c30"
|
||||
integrity sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==
|
||||
dependencies:
|
||||
rndm "1.2.0"
|
||||
tsscmp "1.0.6"
|
||||
uid-safe "2.1.5"
|
||||
|
||||
css-loader@^3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.6.0.tgz#2e4b2c7e6e2d27f8c8f28f61bffcd2e6c91ef645"
|
||||
@ -13302,6 +13316,16 @@ csstype@^3.0.2:
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340"
|
||||
integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==
|
||||
|
||||
csurf@^1.11.0:
|
||||
version "1.11.0"
|
||||
resolved "https://registry.yarnpkg.com/csurf/-/csurf-1.11.0.tgz#ab0c3c6634634192bd3d6f4b861be20800eeb61a"
|
||||
integrity sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==
|
||||
dependencies:
|
||||
cookie "0.4.0"
|
||||
cookie-signature "1.0.6"
|
||||
csrf "3.1.0"
|
||||
http-errors "~1.7.3"
|
||||
|
||||
csv-file-validator@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/csv-file-validator/-/csv-file-validator-2.1.0.tgz#fc83e1e05835d7f03d03f8cce6235938e4cef32e"
|
||||
@ -17927,6 +17951,17 @@ http-errors@~1.6.2:
|
||||
setprototypeof "1.1.0"
|
||||
statuses ">= 1.4.0 < 2"
|
||||
|
||||
http-errors@~1.7.3:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
|
||||
integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
|
||||
dependencies:
|
||||
depd "~1.1.2"
|
||||
inherits "2.0.4"
|
||||
setprototypeof "1.1.1"
|
||||
statuses ">= 1.5.0 < 2"
|
||||
toidentifier "1.0.0"
|
||||
|
||||
http-parser-js@>=0.5.1:
|
||||
version "0.5.5"
|
||||
resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.5.tgz#d7c30d5d3c90d865b4a2e870181f9d6f22ac7ac5"
|
||||
@ -26183,6 +26218,11 @@ randexp@0.4.6:
|
||||
discontinuous-range "1.0.0"
|
||||
ret "~0.1.10"
|
||||
|
||||
random-bytes@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
|
||||
integrity sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==
|
||||
|
||||
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
|
||||
@ -27797,6 +27837,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
|
||||
hash-base "^3.0.0"
|
||||
inherits "^2.0.1"
|
||||
|
||||
rndm@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/rndm/-/rndm-1.2.0.tgz#f33fe9cfb52bbfd520aa18323bc65db110a1b76c"
|
||||
integrity sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==
|
||||
|
||||
rollup@2.78.0:
|
||||
version "2.78.0"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.78.0.tgz#00995deae70c0f712ea79ad904d5f6b033209d9e"
|
||||
@ -28296,6 +28341,11 @@ setprototypeof@1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
|
||||
integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
|
||||
|
||||
setprototypeof@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
|
||||
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
|
||||
|
||||
setprototypeof@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
|
||||
@ -28947,7 +28997,7 @@ statuses@2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
|
||||
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
|
||||
|
||||
"statuses@>= 1.4.0 < 2":
|
||||
"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
||||
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
|
||||
@ -30025,6 +30075,11 @@ toggle-selection@^1.0.6:
|
||||
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
|
||||
integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI=
|
||||
|
||||
toidentifier@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
||||
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
||||
|
||||
toidentifier@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||
@ -30316,6 +30371,11 @@ tslib@~2.4.0:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
|
||||
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
|
||||
|
||||
tsscmp@1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"
|
||||
integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==
|
||||
|
||||
tsutils@^3.21.0:
|
||||
version "3.21.0"
|
||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
|
||||
@ -30580,6 +30640,13 @@ uhyphen@^0.2.0:
|
||||
resolved "https://registry.yarnpkg.com/uhyphen/-/uhyphen-0.2.0.tgz#8fdf0623314486e020a3c00ee5cc7a12fe722b81"
|
||||
integrity sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==
|
||||
|
||||
uid-safe@2.1.5:
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a"
|
||||
integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==
|
||||
dependencies:
|
||||
random-bytes "~1.0.0"
|
||||
|
||||
unbox-primitive@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
|
||||
|
||||
Reference in New Issue
Block a user