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:
Jackson Harper
2024-05-15 11:10:38 +08:00
committed by GitHub
20 changed files with 340 additions and 65 deletions

File diff suppressed because one or more lines are too long

View File

@ -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)
}

View File

@ -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))
}
}

View File

@ -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
}

View File

@ -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 }

View File

@ -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
}

View File

@ -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)
}

View File

@ -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")

View File

@ -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()

View File

@ -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

View File

@ -40,7 +40,6 @@
}
}
@available(iOS 16.0, *)
struct LibraryItemEntity: AppEntity {
static var defaultQuery = LibraryItemQuery()

View File

@ -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>;

View File

@ -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

View File

@ -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']
)

View File

@ -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

View File

@ -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)
})
})
})

View File

@ -27,7 +27,9 @@ export async function updateDigestConfigMutation(
}
... on SetUserPersonalizationSuccess {
updatedUserPersonalization {
digestConfig
digestConfig {
channels
}
}
}
}

View File

@ -58,7 +58,9 @@ export function useGetUserPersonalization(): UserPersonalizationResult {
getUserPersonalization {
... on GetUserPersonalizationSuccess {
userPersonalization {
digestConfig
digestConfig {
channels
}
}
}
... on GetUserPersonalizationError {

View File

@ -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"