Merge pull request #3944 from omnivore-app/feat/ios-display-digest-icon
Feat/ios display digest icon
|
|
@ -2,6 +2,10 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>app.omnivore.omnivore.openURL</string>
|
||||
</array>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
|
|
|
|||
|
|
@ -223,10 +223,11 @@ struct FullScreenDigestView: View {
|
|||
.foregroundColor(Color(hex: "#898989"))
|
||||
.lineLimit(1)
|
||||
} else {
|
||||
Text("We're building you a new digest")
|
||||
.font(Font.system(size: 17, weight: .semibold))
|
||||
.lineLimit(3)
|
||||
ProgressView()
|
||||
HStack {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(15)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@
|
|||
@EnvironmentObject var dataService: DataService
|
||||
@EnvironmentObject var audioController: AudioController
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme: ColorScheme
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
let delete: (_: NSManagedObjectID) -> Void
|
||||
|
|
|
|||
|
|
@ -388,7 +388,6 @@ struct AnimatingCellHeight: AnimatableModifier {
|
|||
}
|
||||
|
||||
ToolbarItemGroup(placement: .barTrailing) {
|
||||
|
||||
if viewModel.appliedFilter?.name == "Deleted" {
|
||||
if viewModel.isEmptyingTrash {
|
||||
ProgressView()
|
||||
|
|
@ -415,6 +414,14 @@ 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
|
||||
Button(
|
||||
|
|
@ -459,7 +466,7 @@ struct AnimatingCellHeight: AnimatableModifier {
|
|||
}
|
||||
).buttonStyle(.plain)
|
||||
.padding(.horizontal, UIDevice.isIPad ? 5 : 0)
|
||||
|
||||
|
||||
Button(
|
||||
action: {
|
||||
searchPresented = true
|
||||
|
|
|
|||
|
|
@ -49,6 +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(UserDefaultKey.lastVisitedDigestId.rawValue) var lastVisitedDigestId = ""
|
||||
@AppStorage("LibraryTabView::digestHidden") var digestHidden = false
|
||||
|
||||
|
|
@ -402,7 +403,7 @@ enum LoadingBarStyle {
|
|||
|
||||
func checkForDigestUpdate(dataService: DataService) async {
|
||||
do {
|
||||
if 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -216,5 +216,8 @@ struct LibraryTabView: View {
|
|||
}
|
||||
selectedTab = "inbox"
|
||||
}
|
||||
.task {
|
||||
await dataService.tryUpdateFeatureFlags()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,11 +30,10 @@ import Views
|
|||
|
||||
init() {}
|
||||
|
||||
func submitProfile(name: String, bio: String, authenticator: Authenticator) {
|
||||
func submitProfile(name: String, authenticator: Authenticator) {
|
||||
let profileOrError = NewUserProfile.make(
|
||||
username: potentialUsername,
|
||||
name: name,
|
||||
bio: bio.isEmpty ? nil : bio
|
||||
name: name
|
||||
)
|
||||
|
||||
switch profileOrError {
|
||||
|
|
@ -112,7 +111,6 @@ struct CreateProfileView: View {
|
|||
@StateObject private var viewModel = CreateProfileViewModel()
|
||||
|
||||
@State private var name = ""
|
||||
@State private var bio = ""
|
||||
|
||||
init(userProfile: NewUserProfile) {
|
||||
self.initialUserProfile = userProfile
|
||||
|
|
@ -171,29 +169,11 @@ struct CreateProfileView: View {
|
|||
}
|
||||
.animation(.default, value: 0.35)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(LocalText.registrationBio)
|
||||
.font(.appFootnote)
|
||||
.foregroundColor(.appGrayText)
|
||||
TextEditor(text: $bio)
|
||||
.lineSpacing(6)
|
||||
.accentColor(.appGraySolid)
|
||||
.foregroundColor(.appGrayText)
|
||||
.font(.appBody)
|
||||
.padding(12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.strokeBorder(Color.appGrayBorder, lineWidth: 1)
|
||||
.background(RoundedRectangle(cornerRadius: 16).fill(Color.systemBackground))
|
||||
)
|
||||
.frame(height: 160)
|
||||
}
|
||||
|
||||
Button(
|
||||
action: { viewModel.submitProfile(name: name, bio: bio, authenticator: authenticator) },
|
||||
action: { viewModel.submitProfile(name: name, authenticator: authenticator) },
|
||||
label: { Text(viewModel.submitButtonText) }
|
||||
)
|
||||
.buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300))
|
||||
.buttonStyle(SolidCapsuleButtonStyle(color: .appCtaYellow, textColor: Color.themeDarkGray, width: 300))
|
||||
|
||||
if let errorMessage = viewModel.validationErrorMessage {
|
||||
Text(errorMessage)
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ struct EmailAuthView: View {
|
|||
|
||||
var innerBody: some View {
|
||||
ZStack {
|
||||
Color.appBackground.edgesIgnoringSafeArea(.all)
|
||||
Color.themeSolidBackground.edgesIgnoringSafeArea(.all)
|
||||
primaryContent
|
||||
.frame(maxWidth: 300)
|
||||
#if os(iOS)
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ struct EmailLoginFormView: View {
|
|||
},
|
||||
label: { Text(LocalText.genericSubmit) }
|
||||
)
|
||||
.buttonStyle(SolidCapsuleButtonStyle(color: .appCtaYellow, width: 300))
|
||||
.buttonStyle(SolidCapsuleButtonStyle(color: .appCtaYellow, textColor: Color.themeDarkGray, width: 300))
|
||||
|
||||
if let loginError = viewModel.loginError {
|
||||
LoginErrorMessageView(loginError: loginError)
|
||||
|
|
@ -186,7 +186,7 @@ struct EmailPendingVerificationView: View {
|
|||
}
|
||||
}
|
||||
)
|
||||
.buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300))
|
||||
.buttonStyle(SolidCapsuleButtonStyle(color: .appCtaYellow, textColor: Color.themeDarkGray, width: 300))
|
||||
|
||||
HStack {
|
||||
Button(
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ struct EmailSignupFormView: View {
|
|||
}
|
||||
}
|
||||
)
|
||||
.buttonStyle(SolidCapsuleButtonStyle(color: .appCtaYellow, width: 300))
|
||||
.buttonStyle(SolidCapsuleButtonStyle(color: .appCtaYellow, textColor: Color.themeDarkGray, width: 300))
|
||||
|
||||
if let loginError = viewModel.loginError {
|
||||
LoginErrorMessageView(loginError: loginError)
|
||||
|
|
|
|||
|
|
@ -50,13 +50,13 @@ struct NewAppleSignupView: View {
|
|||
},
|
||||
label: { Text(LocalText.genericContinue) }
|
||||
)
|
||||
.buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300))
|
||||
.buttonStyle(SolidCapsuleButtonStyle(color: .appCtaYellow, textColor: Color.themeDarkGray, width: 300))
|
||||
|
||||
Button(
|
||||
action: showProfileEditView,
|
||||
label: { Text(LocalText.registrationChangeUsername) }
|
||||
)
|
||||
.buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300))
|
||||
.buttonStyle(SolidCapsuleButtonStyle(color: .appCtaYellow, textColor: Color.themeDarkGray, width: 300))
|
||||
|
||||
if let loginError = viewModel.loginError {
|
||||
LoginErrorMessageView(loginError: loginError)
|
||||
|
|
|
|||
|
|
@ -37,8 +37,6 @@ public struct RootView: View {
|
|||
Services.scheduleBackgroundFetch()
|
||||
#endif
|
||||
}
|
||||
}.task {
|
||||
await viewModel.services.dataService.tryUpdateFeatureFlags()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ struct WebReaderContainerView: View {
|
|||
|
||||
@State var showExplainSheet = false
|
||||
|
||||
@State var handoffActivity: NSUserActivity?
|
||||
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@EnvironmentObject var audioController: AudioController
|
||||
@Environment(\.openURL) var openURL
|
||||
|
|
@ -422,6 +424,8 @@ struct WebReaderContainerView: View {
|
|||
.statusBar(hidden: prefersHideStatusBarInReader)
|
||||
#endif
|
||||
.onAppear {
|
||||
startUserActivity()
|
||||
|
||||
dataService.updateLinkReadingProgress(
|
||||
itemID: item.unwrappedID,
|
||||
readingProgress: max(item.readingProgress, 0.1),
|
||||
|
|
@ -437,6 +441,9 @@ struct WebReaderContainerView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
stopUserActivity()
|
||||
}
|
||||
.confirmationDialog(linkToOpen?.absoluteString ?? "", isPresented: $displayLinkSheet,
|
||||
titleVisibility: .visible) {
|
||||
Button(action: {
|
||||
|
|
@ -708,7 +715,7 @@ struct WebReaderContainerView: View {
|
|||
func share() {
|
||||
shareActionID = UUID()
|
||||
}
|
||||
|
||||
|
||||
func printReader() {
|
||||
shareActionID = UUID()
|
||||
}
|
||||
|
|
@ -752,4 +759,23 @@ struct WebReaderContainerView: View {
|
|||
|
||||
openURL(url)
|
||||
}
|
||||
|
||||
func startUserActivity() {
|
||||
if let slug = item.slug,
|
||||
let webpageURL = URL(string: "\(dataService.appEnvironment.webAppBaseURL)/me/\(slug)") {
|
||||
let activity = NSUserActivity(activityType: "com.omnivore.omnivore.openURL")
|
||||
activity.title = "Open in browser"
|
||||
activity.webpageURL = webpageURL
|
||||
activity.isEligibleForHandoff = true
|
||||
activity.becomeCurrent()
|
||||
handoffActivity = activity
|
||||
}
|
||||
}
|
||||
|
||||
func stopUserActivity() {
|
||||
self.handoffActivity = nil
|
||||
Task {
|
||||
await NSUserActivity.deleteAllSavedUserActivities()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,6 +159,8 @@ struct WelcomeView: View {
|
|||
|
||||
var logoView: some View {
|
||||
Image.omnivoreTitleLogo
|
||||
.renderingMode(.template)
|
||||
.foregroundColor(Color.appGrayTextContrast)
|
||||
.gesture(
|
||||
TapGesture(count: 2)
|
||||
.onEnded {
|
||||
|
|
@ -231,7 +233,7 @@ struct WelcomeView: View {
|
|||
|
||||
public var body: some View {
|
||||
ZStack(alignment: viewModel.registrationState == nil ? .leading : .center) {
|
||||
Color.appBackground
|
||||
Color.themeSolidBackground
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.modifier(SizeModifier())
|
||||
.onPreferenceChange(SizePreferenceKey.self) {
|
||||
|
|
@ -286,7 +288,6 @@ struct WelcomeView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.preferredColorScheme(.light)
|
||||
.task { selectedEnvironment = dataService.appEnvironment }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,13 +20,12 @@ public struct NewUserProfile: Codable {
|
|||
public extension NewUserProfile {
|
||||
static func make(
|
||||
username: String,
|
||||
name: String,
|
||||
bio: String?
|
||||
name: String
|
||||
) -> Either<NewUserProfile, String> {
|
||||
let userProfile = NewUserProfile(
|
||||
username: username,
|
||||
name: name,
|
||||
bio: bio
|
||||
bio: nil
|
||||
)
|
||||
|
||||
if let errorMessage = userProfile.validationErrorMessage {
|
||||
|
|
|
|||
|
|
@ -300,7 +300,7 @@ public final class DataService: ObservableObject {
|
|||
}
|
||||
return objectID
|
||||
}
|
||||
|
||||
|
||||
public func tryUpdateFeatureFlags() async {
|
||||
if let features = (try? await fetchViewer())?.enabledFeatures {
|
||||
featureFlags.digestEnabled = features.contains("ai-digest")
|
||||
|
|
|
|||
|
|
@ -13273,6 +13273,7 @@ extension Objects {
|
|||
let optInFeature: [String: Unions.OptInFeatureResult]
|
||||
let recommend: [String: Unions.RecommendResult]
|
||||
let recommendHighlights: [String: Unions.RecommendHighlightsResult]
|
||||
let replyToEmail: [String: Unions.ReplyToEmailResult]
|
||||
let reportItem: [String: Objects.ReportItemResult]
|
||||
let revokeApiKey: [String: Unions.RevokeApiKeyResult]
|
||||
let saveArticleReadingProgress: [String: Unions.SaveArticleReadingProgressResult]
|
||||
|
|
@ -13475,6 +13476,10 @@ extension Objects.Mutation: Decodable {
|
|||
if let value = try container.decode(Unions.RecommendHighlightsResult?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "replyToEmail":
|
||||
if let value = try container.decode(Unions.ReplyToEmailResult?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "reportItem":
|
||||
if let value = try container.decode(Objects.ReportItemResult?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
|
|
@ -13647,6 +13652,7 @@ extension Objects.Mutation: Decodable {
|
|||
optInFeature = map["optInFeature"]
|
||||
recommend = map["recommend"]
|
||||
recommendHighlights = map["recommendHighlights"]
|
||||
replyToEmail = map["replyToEmail"]
|
||||
reportItem = map["reportItem"]
|
||||
revokeApiKey = map["revokeApiKey"]
|
||||
saveArticleReadingProgress = map["saveArticleReadingProgress"]
|
||||
|
|
@ -14404,6 +14410,25 @@ extension Fields where TypeLock == Objects.Mutation {
|
|||
}
|
||||
}
|
||||
|
||||
func replyToEmail<Type>(recentEmailId: String, reply: Enums.AllowedReply, selection: Selection<Type, Unions.ReplyToEmailResult>) throws -> Type {
|
||||
let field = GraphQLField.composite(
|
||||
name: "replyToEmail",
|
||||
arguments: [Argument(name: "recentEmailId", type: "ID!", value: recentEmailId), Argument(name: "reply", type: "AllowedReply!", value: reply)],
|
||||
selection: selection.selection
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
if let data = data.replyToEmail[field.alias!] {
|
||||
return try selection.decode(data: data)
|
||||
}
|
||||
throw HttpError.badpayload
|
||||
case .mocking:
|
||||
return selection.mock()
|
||||
}
|
||||
}
|
||||
|
||||
func reportItem<Type>(input: InputObjects.ReportItemInput, selection: Selection<Type, Objects.ReportItemResult>) throws -> Type {
|
||||
let field = GraphQLField.composite(
|
||||
name: "reportItem",
|
||||
|
|
@ -17261,6 +17286,8 @@ extension Objects {
|
|||
let from: [String: String]
|
||||
let html: [String: String]
|
||||
let id: [String: String]
|
||||
let reply: [String: String]
|
||||
let replyTo: [String: String]
|
||||
let subject: [String: String]
|
||||
let text: [String: String]
|
||||
let to: [String: String]
|
||||
|
|
@ -17300,6 +17327,14 @@ extension Objects.RecentEmail: Decodable {
|
|||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "reply":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "replyTo":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "subject":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
|
|
@ -17330,6 +17365,8 @@ extension Objects.RecentEmail: Decodable {
|
|||
from = map["from"]
|
||||
html = map["html"]
|
||||
id = map["id"]
|
||||
reply = map["reply"]
|
||||
replyTo = map["replyTo"]
|
||||
subject = map["subject"]
|
||||
text = map["text"]
|
||||
to = map["to"]
|
||||
|
|
@ -17407,6 +17444,36 @@ extension Fields where TypeLock == Objects.RecentEmail {
|
|||
}
|
||||
}
|
||||
|
||||
func reply() throws -> String? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "reply",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return data.reply[field.alias!]
|
||||
case .mocking:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func replyTo() throws -> String? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "replyTo",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return data.replyTo[field.alias!]
|
||||
case .mocking:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func subject() throws -> String {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "subject",
|
||||
|
|
@ -18975,6 +19042,136 @@ extension Selection where TypeLock == Never, Type == Never {
|
|||
typealias ReminderSuccess<T> = Selection<T, Objects.ReminderSuccess>
|
||||
}
|
||||
|
||||
extension Objects {
|
||||
struct ReplyToEmailError {
|
||||
let __typename: TypeName = .replyToEmailError
|
||||
let errorCodes: [String: [Enums.ReplyToEmailErrorCode]]
|
||||
|
||||
enum TypeName: String, Codable {
|
||||
case replyToEmailError = "ReplyToEmailError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Objects.ReplyToEmailError: 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.ReplyToEmailErrorCode]?.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.ReplyToEmailError {
|
||||
func errorCodes() throws -> [Enums.ReplyToEmailErrorCode] {
|
||||
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 ReplyToEmailError<T> = Selection<T, Objects.ReplyToEmailError>
|
||||
}
|
||||
|
||||
extension Objects {
|
||||
struct ReplyToEmailSuccess {
|
||||
let __typename: TypeName = .replyToEmailSuccess
|
||||
let success: [String: Bool]
|
||||
|
||||
enum TypeName: String, Codable {
|
||||
case replyToEmailSuccess = "ReplyToEmailSuccess"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Objects.ReplyToEmailSuccess: 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 "success":
|
||||
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)."
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
success = map["success"]
|
||||
}
|
||||
}
|
||||
|
||||
extension Fields where TypeLock == Objects.ReplyToEmailSuccess {
|
||||
func success() throws -> Bool {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "success",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
if let data = data.success[field.alias!] {
|
||||
return data
|
||||
}
|
||||
throw HttpError.badpayload
|
||||
case .mocking:
|
||||
return Bool.mockValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Selection where TypeLock == Never, Type == Never {
|
||||
typealias ReplyToEmailSuccess<T> = Selection<T, Objects.ReplyToEmailSuccess>
|
||||
}
|
||||
|
||||
extension Objects {
|
||||
struct ReportItemResult {
|
||||
let __typename: TypeName = .reportItemResult
|
||||
|
|
@ -20445,6 +20642,7 @@ extension Objects {
|
|||
let directionality: [String: Enums.DirectionalityType]
|
||||
let feedContent: [String: String]
|
||||
let folder: [String: String]
|
||||
let format: [String: String]
|
||||
let highlights: [String: [Objects.Highlight]]
|
||||
let id: [String: String]
|
||||
let image: [String: String]
|
||||
|
|
@ -20545,6 +20743,10 @@ extension Objects.SearchItem: Decodable {
|
|||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "format":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "highlights":
|
||||
if let value = try container.decode([Objects.Highlight]?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
|
|
@ -20699,6 +20901,7 @@ extension Objects.SearchItem: Decodable {
|
|||
directionality = map["directionality"]
|
||||
feedContent = map["feedContent"]
|
||||
folder = map["folder"]
|
||||
format = map["format"]
|
||||
highlights = map["highlights"]
|
||||
id = map["id"]
|
||||
image = map["image"]
|
||||
|
|
@ -20925,6 +21128,21 @@ extension Fields where TypeLock == Objects.SearchItem {
|
|||
}
|
||||
}
|
||||
|
||||
func format() throws -> String? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "format",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return data.format[field.alias!]
|
||||
case .mocking:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func highlights<Type>(selection: Selection<Type, [Objects.Highlight]?>) throws -> Type {
|
||||
let field = GraphQLField.composite(
|
||||
name: "highlights",
|
||||
|
|
@ -27785,6 +28003,7 @@ extension Selection where TypeLock == Never, Type == Never {
|
|||
extension Objects {
|
||||
struct UserPersonalization {
|
||||
let __typename: TypeName = .userPersonalization
|
||||
let digestConfig: [String: String]
|
||||
let fields: [String: String]
|
||||
let fontFamily: [String: String]
|
||||
let fontSize: [String: Int]
|
||||
|
|
@ -27816,6 +28035,10 @@ extension Objects.UserPersonalization: Decodable {
|
|||
let field = GraphQLField.getFieldNameFromAlias(alias)
|
||||
|
||||
switch field {
|
||||
case "digestConfig":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "fields":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
|
|
@ -27874,6 +28097,7 @@ extension Objects.UserPersonalization: Decodable {
|
|||
}
|
||||
}
|
||||
|
||||
digestConfig = map["digestConfig"]
|
||||
fields = map["fields"]
|
||||
fontFamily = map["fontFamily"]
|
||||
fontSize = map["fontSize"]
|
||||
|
|
@ -27890,6 +28114,21 @@ extension Objects.UserPersonalization: Decodable {
|
|||
}
|
||||
|
||||
extension Fields where TypeLock == Objects.UserPersonalization {
|
||||
func digestConfig() throws -> String? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "digestConfig",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return data.digestConfig[field.alias!]
|
||||
case .mocking:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func fields() throws -> String? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "fields",
|
||||
|
|
@ -33777,6 +34016,80 @@ extension Selection where TypeLock == Never, Type == Never {
|
|||
typealias ReminderResult<T> = Selection<T, Unions.ReminderResult>
|
||||
}
|
||||
|
||||
extension Unions {
|
||||
struct ReplyToEmailResult {
|
||||
let __typename: TypeName
|
||||
let errorCodes: [String: [Enums.ReplyToEmailErrorCode]]
|
||||
let success: [String: Bool]
|
||||
|
||||
enum TypeName: String, Codable {
|
||||
case replyToEmailError = "ReplyToEmailError"
|
||||
case replyToEmailSuccess = "ReplyToEmailSuccess"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Unions.ReplyToEmailResult: 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.ReplyToEmailErrorCode]?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "success":
|
||||
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")!)
|
||||
|
||||
errorCodes = map["errorCodes"]
|
||||
success = map["success"]
|
||||
}
|
||||
}
|
||||
|
||||
extension Fields where TypeLock == Unions.ReplyToEmailResult {
|
||||
func on<Type>(replyToEmailError: Selection<Type, Objects.ReplyToEmailError>, replyToEmailSuccess: Selection<Type, Objects.ReplyToEmailSuccess>) throws -> Type {
|
||||
select([GraphQLField.fragment(type: "ReplyToEmailError", selection: replyToEmailError.selection), GraphQLField.fragment(type: "ReplyToEmailSuccess", selection: replyToEmailSuccess.selection)])
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
switch data.__typename {
|
||||
case .replyToEmailError:
|
||||
let data = Objects.ReplyToEmailError(errorCodes: data.errorCodes)
|
||||
return try replyToEmailError.decode(data: data)
|
||||
case .replyToEmailSuccess:
|
||||
let data = Objects.ReplyToEmailSuccess(success: data.success)
|
||||
return try replyToEmailSuccess.decode(data: data)
|
||||
}
|
||||
case .mocking:
|
||||
return replyToEmailError.mock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Selection where TypeLock == Never, Type == Never {
|
||||
typealias ReplyToEmailResult<T> = Selection<T, Unions.ReplyToEmailResult>
|
||||
}
|
||||
|
||||
extension Unions {
|
||||
struct RevokeApiKeyResult {
|
||||
let __typename: TypeName
|
||||
|
|
@ -37206,6 +37519,19 @@ extension Enums {
|
|||
}
|
||||
}
|
||||
|
||||
extension Enums {
|
||||
/// AllowedReply
|
||||
enum AllowedReply: String, CaseIterable, Codable {
|
||||
case confirm = "CONFIRM"
|
||||
|
||||
case okay = "OKAY"
|
||||
|
||||
case subscribe = "SUBSCRIBE"
|
||||
|
||||
case yes = "YES"
|
||||
}
|
||||
}
|
||||
|
||||
extension Enums {
|
||||
/// ApiKeysErrorCode
|
||||
enum ApiKeysErrorCode: String, CaseIterable, Codable {
|
||||
|
|
@ -37911,6 +38237,8 @@ extension Enums {
|
|||
enum OptInFeatureErrorCode: String, CaseIterable, Codable {
|
||||
case badRequest = "BAD_REQUEST"
|
||||
|
||||
case ineligible = "INELIGIBLE"
|
||||
|
||||
case notFound = "NOT_FOUND"
|
||||
}
|
||||
}
|
||||
|
|
@ -38008,6 +38336,13 @@ extension Enums {
|
|||
}
|
||||
}
|
||||
|
||||
extension Enums {
|
||||
/// ReplyToEmailErrorCode
|
||||
enum ReplyToEmailErrorCode: String, CaseIterable, Codable {
|
||||
case unauthorized = "UNAUTHORIZED"
|
||||
}
|
||||
}
|
||||
|
||||
extension Enums {
|
||||
/// ReportType
|
||||
enum ReportType: String, CaseIterable, Codable {
|
||||
|
|
@ -40044,6 +40379,8 @@ extension InputObjects {
|
|||
|
||||
extension InputObjects {
|
||||
struct SetUserPersonalizationInput: Encodable, Hashable {
|
||||
var digestConfig: OptionalArgument<String> = .absent()
|
||||
|
||||
var fields: OptionalArgument<String> = .absent()
|
||||
|
||||
var fontFamily: OptionalArgument<String> = .absent()
|
||||
|
|
@ -40068,6 +40405,7 @@ extension InputObjects {
|
|||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
if digestConfig.hasValue { try container.encode(digestConfig, forKey: .digestConfig) }
|
||||
if fields.hasValue { try container.encode(fields, forKey: .fields) }
|
||||
if fontFamily.hasValue { try container.encode(fontFamily, forKey: .fontFamily) }
|
||||
if fontSize.hasValue { try container.encode(fontSize, forKey: .fontSize) }
|
||||
|
|
@ -40082,6 +40420,7 @@ extension InputObjects {
|
|||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case digestConfig
|
||||
case fields
|
||||
case fontFamily
|
||||
case fontSize
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ public struct Feature {
|
|||
public let granted: Bool
|
||||
}
|
||||
|
||||
public enum IneligibleError: Error {
|
||||
case message(messageText: String)
|
||||
}
|
||||
|
||||
public extension DataService {
|
||||
func optInFeature(name: String) async throws -> Feature? {
|
||||
enum MutationResult {
|
||||
|
|
@ -51,6 +55,9 @@ public extension DataService {
|
|||
case let .success(feature: feature):
|
||||
continuation.resume(returning: feature)
|
||||
case let .error(errorCode: errorCode):
|
||||
if errorCode == .ineligible {
|
||||
continuation.resume(throwing: IneligibleError.message(messageText: "You are not eligible for this feature."))
|
||||
}
|
||||
continuation.resume(throwing: BasicError.message(messageText: errorCode.rawValue))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,5 @@ public struct AppleSignInButton: View {
|
|||
.frame(height: 54)
|
||||
.frame(maxWidth: 300)
|
||||
.cornerRadius(8)
|
||||
.signInWithAppleButtonStyle(.white)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 532 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 711 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 7.5 KiB |