diff --git a/src/core/src/client/contexts/session/SessionContext.tsx b/src/core/src/client/contexts/session/SessionContext.tsx index 0ebb05545..173d8fdc1 100644 --- a/src/core/src/client/contexts/session/SessionContext.tsx +++ b/src/core/src/client/contexts/session/SessionContext.tsx @@ -18,7 +18,6 @@ import { ReadingFormat, RecapAttributes, } from '~/api'; -import { Locale, getLocale } from '~/locales'; import { useLocalStorage, usePlatformTools } from '~/utils'; export const SessionContext = React.createContext(DEFAULT_SESSION_CONTEXT); @@ -41,7 +40,6 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { const [lastRequestForReview, setLastRequestForReview] = React.useState(0); const [categories, setCategories] = React.useState>(); const [publishers, setPublishers] = React.useState>(); - const [loadedInitialUrl, setLoadedInitialUrl] = React.useState(false); // user state const [uuid, setUuid] = React.useState(); @@ -53,7 +51,6 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { const [bookmarkedSummaries, setBookmarkedSummaries] = React.useState<{ [key: number]: Bookmark }>(); const [readSummaries, setReadSummaries] = React.useState<{ [key: number]: Bookmark }>(); const [removedSummaries, setRemovedSummaries] = React.useState<{ [key: number]: boolean }>(); - const [locale, setLocale] = React.useState(); const [summaryTranslations, setSummaryTranslations] = React.useState<{ [key: number]: { [key in keyof PublicSummaryGroup]?: string } }>(); // bookmark state @@ -169,9 +166,6 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { case 'lastRequestForReview': setLastRequestForReview(newValue); break; - case 'loadedInitialUrl': - setLoadedInitialUrl(newValue); - break; // user state case 'uuid': @@ -551,13 +545,11 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { setBookmarkedSummaries(await getPreference('bookmarkedSummaries')); setReadSummaries(await getPreference('readSummaries')); setRemovedSummaries(await getPreference('removedSummaries')); - const locale = await getPreference('locale'); - setLocale(locale); - setSummaryTranslations(locale !== getLocale() ? {} : await getPreference('summaryTranslations')); + setSummaryTranslations(await getPreference('summaryTranslations')); // recap state setReadRecaps(await getPreference('readRecaps')); - setRecapTranslations(locale !== getLocale() ? {} : await getPreference('recapTranslations')); + setRecapTranslations(await getPreference('recapTranslations')); // publisher states setFollowedPublishers(await getPreference('followedPublishers')); @@ -575,7 +567,7 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { setFontSizeOffset(await getPreference('fontSizeOffset')); setLetterSpacing(await getPreference('letterSpacing')); setLineHeightMultiplier(await getPreference('lineHeightMultiplier')); - + // summary preferences setCompactSummaries(await getPreference('compactSummaries') ?? await getPreference('compactMode')); setShowShortSummary(await getPreference('showShortSummary')); @@ -637,7 +629,6 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { latestVersion, letterSpacing, lineHeightMultiplier, - loadedInitialUrl, preferredReadingFormat, preferredShortPressFormat, publisherIsFavorited, diff --git a/src/core/src/client/contexts/session/types.ts b/src/core/src/client/contexts/session/types.ts index e8e2fa3ec..80ef2fd67 100644 --- a/src/core/src/client/contexts/session/types.ts +++ b/src/core/src/client/contexts/session/types.ts @@ -8,7 +8,6 @@ import { RecapAttributes, RequestParams, } from '~/api'; -import { Locale } from '~/locales'; export type BookmarkConstructorProps = { createdAt: Date; @@ -118,7 +117,6 @@ export type Preferences = { viewedFeatures?: { [key: string]: Bookmark }; hasReviewed?: boolean; lastRequestForReview: number; - loadedInitialUrl?: boolean; // user state uuid?: string; @@ -132,7 +130,6 @@ export type Preferences = { bookmarkCount: number; unreadBookmarkCount: number; removedSummaries?: { [key: number]: boolean }; - locale?: Locale; summaryTranslations?: { [key: number]: { [key in keyof PublicSummaryGroup]?: string } }; // recap state @@ -192,8 +189,6 @@ export const PREFERENCE_TYPES: { [key in keyof Preferences]: 'boolean' | 'number lastRequestForReview: 'number', letterSpacing: 'number', lineHeightMultiplier: 'number', - loadedInitialUrl: 'boolean', - locale: 'string', preferredReadingFormat: 'string', preferredShortPressFormat: 'string', pushNotifications: 'object', diff --git a/src/mobile/android/app/build.gradle b/src/mobile/android/app/build.gradle index 7129006f2..e6e11e63f 100644 --- a/src/mobile/android/app/build.gradle +++ b/src/mobile/android/app/build.gradle @@ -98,8 +98,8 @@ android { applicationId "ai.readless.ReadLess" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 99 - versionName "1.16.0" + versionCode 98 + versionName "1.15.2" missingDimensionStrategy "store", "play" } diff --git a/src/mobile/ios/Extensions/Date.swift b/src/mobile/ios/Extensions/Date.swift index c76c2f65f..6a4f2897e 100644 --- a/src/mobile/ios/Extensions/Date.swift +++ b/src/mobile/ios/Extensions/Date.swift @@ -11,11 +11,11 @@ extension Date { func distanceFromNow() -> String { let interval = Calendar.current.dateComponents([.minute, .hour, .day], from: self, to: Date()) if let day = interval.day, day > 0 { - return "\(day)d" + return "\(day) day\(day == 1 ? "" : "s") ago" } else if let hour = interval.hour, hour > 0 { - return "\(hour)h" + return "\(hour) hour\(hour == 1 ? "" : "s") ago" } else if let minute = interval.minute, minute > 0 { - return "\(minute)m" + return "\(minute) minute\(minute == 1 ? "" : "s") ago" } else { return "just now" } diff --git a/src/mobile/ios/Extensions/Image.swift b/src/mobile/ios/Extensions/Image.swift deleted file mode 100644 index 98617e976..000000000 --- a/src/mobile/ios/Extensions/Image.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// Image.swift -// ReadLess -// -// Created by thom on 10/4/23. -// - -import Foundation -import CoreGraphics -import SwiftUI - -public extension Image { - - static func loadAsync(from string: String, maxWidth: CGFloat? = nil) async -> Image? { - guard let url = URL(string: string) else { return nil } - return await self.loadAsync(from: url, maxWidth: maxWidth) - } - - static func loadAsync(from url: URL?, maxWidth: CGFloat? = nil) async -> Image? { - guard let url = url else { return nil } - let request = URLRequest(url: url) - guard let (data, _) = try? await URLSession.shared.data(for: request) else { return nil } - guard let image = UIImage(data: data) else { return nil } - if let maxWidth = maxWidth, let image = image.resized(toWidth: maxWidth) { - return Image(uiImage: image) - } - return Image(uiImage: image) - } - - static func load(from string: String, maxWidth: CGFloat? = nil, completion: @escaping @Sendable (_ image: Image?) -> Void) { - guard let imageUrl = URL(string: string) else { return } - return self.load(from: imageUrl, completion: completion) - } - - static func load(from url: URL?, maxWidth: CGFloat? = nil, completion: @escaping @Sendable (_ image: Image?) -> Void) { - guard let url = url else { - completion(nil) - return - } - URLSession.shared.dataTask(with: url) { data, _, error in - if let data = data, let image = UIImage(data: data) { - if let maxWidth = maxWidth, let image = image.resized(toWidth: maxWidth) { - completion(Image(uiImage: image)) - } else { - completion(Image(uiImage: image)) - } - } - }.resume() - } - -} diff --git a/src/mobile/ios/Extensions/UIImage.swift b/src/mobile/ios/Extensions/UIImage.swift deleted file mode 100644 index 51e640446..000000000 --- a/src/mobile/ios/Extensions/UIImage.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// UIImage.swift -// ReadLess -// -// Created by thom on 10/6/23. -// - -import Foundation -import UIKit - -extension UIImage { - - func resized(toWidth width: CGFloat, isOpaque: Bool = true) -> UIImage? { - let size = CGSize(width: width, height: width / size.width * size.height) - return self.resized(toSize: size, isOpaque: isOpaque) - } - - func resized(toSize size: CGSize, isOpaque: Bool = true) -> UIImage? { - UIGraphicsBeginImageContextWithOptions(size, true, 1.0) - self.draw(in: CGRect(origin: .zero, size: size)) - defer { UIGraphicsEndImageContext() } - return UIGraphicsGetImageFromCurrentImageContext() - } - -} diff --git a/src/mobile/ios/Models/BulkMetadataResponse.swift b/src/mobile/ios/Models/BulkResponse.swift similarity index 67% rename from src/mobile/ios/Models/BulkMetadataResponse.swift rename to src/mobile/ios/Models/BulkResponse.swift index 043e843ca..f947f25a6 100644 --- a/src/mobile/ios/Models/BulkMetadataResponse.swift +++ b/src/mobile/ios/Models/BulkResponse.swift @@ -10,31 +10,7 @@ import Foundation import AnyCodable #endif -public struct BulkResponse: Codable { - - public var count: Int - public var rows: [T] - - public init(count: Int, rows: [T]) { - self.count = count - self.rows = rows - } - - public enum CodingKeys: String, CodingKey, CaseIterable { - case count - case rows - } - - // Encodable protocol methods - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(count, forKey: .count) - try container.encode(rows, forKey: .rows) - } -} - -public struct BulkMetadataResponse: Codable { +public struct BulkResponse: Codable { public var count: Int public var rows: [T] diff --git a/src/mobile/ios/Models/PublicPublisherAttributes.swift b/src/mobile/ios/Models/PublicPublisherAttributes.swift index 875f02915..744107cfe 100644 --- a/src/mobile/ios/Models/PublicPublisherAttributes.swift +++ b/src/mobile/ios/Models/PublicPublisherAttributes.swift @@ -16,8 +16,8 @@ public struct PublicPublisherAttributes: Codable, Hashable { public var displayName: String public var description: String? - public var icon: URL { - return URL(string: "https://readless.nyc3.cdn.digitaloceanspaces.com/img/pub/\(self.name).png")! + public var icon: String { + return "https://readless.nyc3.cdn.digitaloceanspaces.com/img/pub/\(self.name).png" } public init(name: String, displayName: String, description: String? = nil) { diff --git a/src/mobile/ios/Models/PublicSummaryAttributes.swift b/src/mobile/ios/Models/PublicSummaryAttributes.swift index 184d66630..66818f5bd 100644 --- a/src/mobile/ios/Models/PublicSummaryAttributes.swift +++ b/src/mobile/ios/Models/PublicSummaryAttributes.swift @@ -6,7 +6,6 @@ // import Foundation -import SwiftUI #if canImport(AnyCodable) import AnyCodable #endif @@ -78,102 +77,4 @@ public struct PublicSummaryAttributes: Codable, Hashable { } -public class Summary { - - public var root: PublicSummaryAttributes - public var id: Int - public var url: String - public var title: String - public var shortSummary: String? - public var publisher: PublicPublisherAttributes - public var category: PublicCategoryAttributes - public var imageUrl: String? - public var media: [String: String]? - public var originalDate: Date? - public var translations: [String: String]? - - public var deeplink: URL { - return URL(string: "https://readless.ai/read/?s=\(id)")! - } - - public var primaryImageUrl: URL? { - return URL(string: media?["imageArticle"] ?? media?["imageAi1"] ?? imageUrl ?? "") - } - - public init(_ summary: PublicSummaryAttributes) { - self.root = summary - self.id = summary.id - self.url = summary.url - self.title = summary.title - self.shortSummary = summary.shortSummary - self.publisher = summary.publisher - self.category = summary.category - self.imageUrl = summary.imageUrl - self.media = summary.media - self.originalDate = summary.originalDate - self.translations = summary.translations - } - - @Published public var image: Image? - @Published public var publisherIcon: Image? - - public func loadImages() { - Image.load(from: primaryImageUrl, maxWidth: 400) { self.image = $0 } - Image.load(from: publisher.icon) { self.publisherIcon = $0 } - } - - public func loadImagesAsync() async { - image = await Image.loadAsync(from: primaryImageUrl, maxWidth: 400) - publisherIcon = await Image.loadAsync(from: publisher.icon) - } - -} - - -public var MOCK_SUMMARY_1 = Summary( - PublicSummaryAttributes(id: 1, - url: "https://readless.ai", - title: "Summary Preview", - shortSummary: "Short Summary", - publisher: PublicPublisherAttributes(name: "cnn", - displayName: "CNN"), - category: PublicCategoryAttributes(name: "sports", - displayName: "Sports", - icon: "basketball") - )) - -public var MOCK_SUMMARY_2 = Summary( - PublicSummaryAttributes(id: 2, - url: "https://readless.ai", - title: "Summary Preview", - shortSummary: "Short Summary", - publisher: PublicPublisherAttributes(name: "forbes", - displayName: "Forbes"), - category: PublicCategoryAttributes(name: "politics", - displayName: "politics", - icon: "bank") - )) - -public var MOCK_SUMMARY_3 = Summary( - PublicSummaryAttributes(id: 3, - url: "https://readless.ai", - title: "Summary Preview", - shortSummary: "Short Summary", - publisher: PublicPublisherAttributes(name: "forbes", - displayName: "Forbes"), - category: PublicCategoryAttributes(name: "politics", - displayName: "politics", - icon: "bank") - )) - -public var MOCK_SUMMARY_4 = Summary( - PublicSummaryAttributes(id: 4, - url: "https://readless.ai", - title: "Summary Preview", - shortSummary: "Short Summary", - publisher: PublicPublisherAttributes(name: "forbes", - displayName: "Forbes"), - category: PublicCategoryAttributes(name: "politics", - displayName: "politics", - icon: "bank") - )) +public var MOCK_SUMMARY = PublicSummaryAttributes(id: 1, url: "https://readless.ai", title: "Summary Preview", shortSummary: "Short Summary", publisher: PublicPublisherAttributes(name: "cnn", displayName: "CNN"), category: PublicCategoryAttributes(name: "sports", displayName: "Sports", icon: "basketball")) diff --git a/src/mobile/ios/ReadLess.xcodeproj/project.pbxproj b/src/mobile/ios/ReadLess.xcodeproj/project.pbxproj index ad09db1ae..a10d7e04c 100644 --- a/src/mobile/ios/ReadLess.xcodeproj/project.pbxproj +++ b/src/mobile/ios/ReadLess.xcodeproj/project.pbxproj @@ -35,7 +35,7 @@ 0125BD5229D791ED00EAC61C /* EvilIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0125BD3229D791EC00EAC61C /* EvilIcons.ttf */; }; 0125BD5829D791ED00EAC61C /* SimpleLineIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0125BD3829D791EC00EAC61C /* SimpleLineIcons.ttf */; }; 0125BD5929D791ED00EAC61C /* MaterialIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0125BD3929D791EC00EAC61C /* MaterialIcons.ttf */; }; - 013F32C329E8BA2E00CE8555 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 013F32C229E8BA2D00CE8555 /* APIClient.swift */; }; + 013F32C329E8BA2E00CE8555 /* ConnectService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 013F32C229E8BA2D00CE8555 /* ConnectService.swift */; }; 0143021C29FACEDC00D40293 /* FontAwesome5_Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0125BD2C29D791EC00EAC61C /* FontAwesome5_Regular.ttf */; }; 0143021D29FACEDC00D40293 /* Zocial.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0125BD3129D791EC00EAC61C /* Zocial.ttf */; }; 0143021E29FACEDC00D40293 /* FontAwesome5_Solid.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0125BD2B29D791EC00EAC61C /* FontAwesome5_Solid.ttf */; }; @@ -54,8 +54,9 @@ 0143022E29FACEDC00D40293 /* MaterialIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0125BD3929D791EC00EAC61C /* MaterialIcons.ttf */; }; 015A19D129EAEB1A0072EEE3 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 015A19D029EAEB1A0072EEE3 /* View.swift */; }; 018173BF29EA1842006D3509 /* PressActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018173BE29EA1842006D3509 /* PressActions.swift */; }; + 018173C129EA4529006D3509 /* SummaryCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018173C029EA4529006D3509 /* SummaryCard.swift */; }; 018173C329EA4833006D3509 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018173C229EA4833006D3509 /* Date.swift */; }; - 018CE3B029E98AB9005E6391 /* BulkMetadataResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018CE3AF29E98AB9005E6391 /* BulkMetadataResponse.swift */; }; + 018CE3B029E98AB9005E6391 /* BulkResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018CE3AF29E98AB9005E6391 /* BulkResponse.swift */; }; 01BC567C29DCF36700C69F9A /* main.jsbundle in Resources */ = {isa = PBXBuildFile; fileRef = 01BC567B29DCF36700C69F9A /* main.jsbundle */; }; 07A1D8E328E1A8A03D5EA3A8 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAFCC9631D60DF42ACBD13 /* ExpoModulesProvider.swift */; }; 0C80B921A6F3F58F76C31292 /* libPods-ReadLess.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-ReadLess.a */; }; @@ -64,12 +65,7 @@ 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 5FFC8B3A3B346CCC17615C08 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA5C7041D39DDE482042B57D /* ExpoModulesProvider.swift */; }; 7699B88040F8A987B510C191 /* libPods-ReadLess-ReadLessTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19F6CBCC0A4E27FBF8BF4A61 /* libPods-ReadLess-ReadLessTests.a */; }; - CD4BF5A52AD074F7006BDC35 /* WidgetTopicConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BF5A42AD074F7006BDC35 /* WidgetTopicConfiguration.swift */; }; - CD4BF5A62AD0797C006BDC35 /* WidgetTopicConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BF5A42AD074F7006BDC35 /* WidgetTopicConfiguration.swift */; }; CD78217A2A28FA2100F3B33A /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD7821792A28FA2100F3B33A /* StoreKit.framework */; }; - CD92BEB22AD093940096D6DA /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = CDE160082ACC8CDB0036F0A4 /* Intents.intentdefinition */; }; - CDA6AD302AD22FD900514CD1 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA6AD2F2AD22FD900514CD1 /* Channel.swift */; }; - CDA6AD312AD22FD900514CD1 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA6AD2F2AD22FD900514CD1 /* Channel.swift */; }; CDE11B1F2A7701BE00C0BC39 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = CDE11B1E2A7701BE00C0BC39 /* GoogleService-Info.plist */; }; CDE11B212A786FF500C0BC39 /* PublicPublisherAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE11B202A786FF500C0BC39 /* PublicPublisherAttributes.swift */; }; CDE15EEA2ACB27A00036F0A4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CDE15EEB2ACB27A00036F0A4 /* LaunchScreen.storyboard */; }; @@ -81,13 +77,13 @@ CDE15FC72ACB497F0036F0A4 /* ReadLessWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = CDE15FB82ACB497E0036F0A4 /* ReadLessWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; CDE15FCB2ACB498E0036F0A4 /* PublicPublisherAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE11B202A786FF500C0BC39 /* PublicPublisherAttributes.swift */; }; CDE15FCC2ACB498E0036F0A4 /* PublicSummaryAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 010F0D2529E88EC40064DD97 /* PublicSummaryAttributes.swift */; }; - CDE15FCD2ACB498E0036F0A4 /* BulkMetadataResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018CE3AF29E98AB9005E6391 /* BulkMetadataResponse.swift */; }; + CDE15FCD2ACB498E0036F0A4 /* BulkResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018CE3AF29E98AB9005E6391 /* BulkResponse.swift */; }; CDE15FCE2ACB498E0036F0A4 /* PublicCategoryAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 010F0D2129E88EC40064DD97 /* PublicCategoryAttributes.swift */; }; CDE15FCF2ACB49920036F0A4 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 010F0D8029E89D770064DD97 /* Color.swift */; }; CDE15FD02ACB49920036F0A4 /* PressActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018173BE29EA1842006D3509 /* PressActions.swift */; }; CDE15FD12ACB49920036F0A4 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 015A19D029EAEB1A0072EEE3 /* View.swift */; }; CDE15FD22ACB49920036F0A4 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018173C229EA4833006D3509 /* Date.swift */; }; - CDE15FD32ACB49950036F0A4 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 013F32C229E8BA2D00CE8555 /* APIClient.swift */; }; + CDE15FD32ACB49950036F0A4 /* ConnectService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 013F32C229E8BA2D00CE8555 /* ConnectService.swift */; }; CDE15FD42ACB49970036F0A4 /* SummaryCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018173C029EA4529006D3509 /* SummaryCard.swift */; }; CDE15FD52ACB499E0036F0A4 /* FontAwesome.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0125BD2F29D791EC00EAC61C /* FontAwesome.ttf */; }; CDE15FD62ACB499E0036F0A4 /* Ionicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0125BD2A29D791EC00EAC61C /* Ionicons.ttf */; }; @@ -119,14 +115,6 @@ CDE15FF02ACB499E0036F0A4 /* Faustina-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = CDE9EB412A48A3770057217B /* Faustina-Italic.ttf */; }; CDE15FF12ACB499E0036F0A4 /* Newsreader72pt-BoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = CDE9EB402A48A3770057217B /* Newsreader72pt-BoldItalic.ttf */; }; CDE15FF22ACB499E0036F0A4 /* AntDesign.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0125BD2529D791EC00EAC61C /* AntDesign.ttf */; }; - CDE160092ACC8CDB0036F0A4 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = CDE160082ACC8CDB0036F0A4 /* Intents.intentdefinition */; }; - CDE160102ACDEF770036F0A4 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE1600F2ACDEF770036F0A4 /* Image.swift */; }; - CDE160112ACDEF770036F0A4 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE1600F2ACDEF770036F0A4 /* Image.swift */; }; - CDE160122ACE08120036F0A4 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; - CDE160132ACE08130036F0A4 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; - CDE1AC1D2AD04CB800F0E430 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE1AC1C2AD04CB800F0E430 /* UIImage.swift */; }; - CDE1AC1E2AD04CB800F0E430 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE1AC1C2AD04CB800F0E430 /* UIImage.swift */; }; - CDE1AC252AD0585D00F0E430 /* SummaryCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018173C029EA4529006D3509 /* SummaryCard.swift */; }; CDE9EB4E2A48A3770057217B /* Newsreader72pt-BoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = CDE9EB402A48A3770057217B /* Newsreader72pt-BoldItalic.ttf */; }; CDE9EB4F2A48A3770057217B /* Newsreader72pt-BoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = CDE9EB402A48A3770057217B /* Newsreader72pt-BoldItalic.ttf */; }; CDE9EB502A48A3770057217B /* Faustina-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = CDE9EB412A48A3770057217B /* Faustina-Italic.ttf */; }; @@ -218,16 +206,6 @@ name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; }; - CDE160072ACC8C390036F0A4 /* Embed ExtensionKit Extensions */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = "$(EXTENSIONS_FOLDER_PATH)"; - dstSubfolderSpec = 16; - files = ( - ); - name = "Embed ExtensionKit Extensions"; - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -264,7 +242,7 @@ 0125BD3229D791EC00EAC61C /* EvilIcons.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = EvilIcons.ttf; sourceTree = ""; }; 0125BD3829D791EC00EAC61C /* SimpleLineIcons.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = SimpleLineIcons.ttf; sourceTree = ""; }; 0125BD3929D791EC00EAC61C /* MaterialIcons.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = MaterialIcons.ttf; sourceTree = ""; }; - 013F32C229E8BA2D00CE8555 /* APIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = ""; }; + 013F32C229E8BA2D00CE8555 /* ConnectService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectService.swift; sourceTree = ""; }; 0150ACA429F9577400875CF7 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 0150ACA629F9577400875CF7 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 015A19D029EAEB1A0072EEE3 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; @@ -273,7 +251,7 @@ 018173C229EA4833006D3509 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; 018CE3AD29E9892B005E6391 /* ReadLessWatch-Watch-App-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "ReadLessWatch-Watch-App-Info.plist"; sourceTree = SOURCE_ROOT; }; 018CE3AE29E989F5005E6391 /* ReadLessWatch Watch App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "ReadLessWatch Watch App.entitlements"; sourceTree = ""; }; - 018CE3AF29E98AB9005E6391 /* BulkMetadataResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BulkMetadataResponse.swift; sourceTree = ""; }; + 018CE3AF29E98AB9005E6391 /* BulkResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BulkResponse.swift; sourceTree = ""; }; 01BC567B29DCF36700C69F9A /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = main.jsbundle; path = ReadLess/main.jsbundle; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* ReadLess.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReadLess.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = ReadLess/AppDelegate.h; sourceTree = ""; }; @@ -286,7 +264,6 @@ 5B7EB9410499542E8C5724F5 /* Pods-ReadLess-ReadLessTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReadLess-ReadLessTests.debug.xcconfig"; path = "Target Support Files/Pods-ReadLess-ReadLessTests/Pods-ReadLess-ReadLessTests.debug.xcconfig"; sourceTree = ""; }; 5DCACB8F33CDC322A6C60F78 /* libPods-ReadLess.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReadLess.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 89C6BE57DB24E9ADA2F236DE /* Pods-ReadLess-ReadLessTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReadLess-ReadLessTests.release.xcconfig"; path = "Target Support Files/Pods-ReadLess-ReadLessTests/Pods-ReadLess-ReadLessTests.release.xcconfig"; sourceTree = ""; }; - CD4BF5A42AD074F7006BDC35 /* WidgetTopicConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WidgetTopicConfiguration.swift; path = /Users/thom/git/noodleofdeath/readless/src/mobile/ios/ReadLessWidget/WidgetTopicConfiguration.swift; sourceTree = ""; }; CD7821792A28FA2100F3B33A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; CD84722A2ACB266C00683959 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Base; path = Base.lproj/Info.plist; sourceTree = ""; }; CD84722C2ACB268E00683959 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = en; path = en.lproj/Info.plist; sourceTree = ""; }; @@ -333,7 +310,6 @@ CD8472562ACB26CD00683959 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = tr; path = tr.lproj/Info.plist; sourceTree = ""; }; CD8472572ACB26CD00683959 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = uk; path = uk.lproj/Info.plist; sourceTree = ""; }; CD8472582ACB26CE00683959 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = vi; path = vi.lproj/Info.plist; sourceTree = ""; }; - CDA6AD2F2AD22FD900514CD1 /* Channel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Channel.swift; sourceTree = ""; }; CDE11B1E2A7701BE00C0BC39 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; CDE11B202A786FF500C0BC39 /* PublicPublisherAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublicPublisherAttributes.swift; sourceTree = ""; }; CDE15EEB2ACB27A00036F0A4 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = ReadLess/LaunchScreen.storyboard; sourceTree = ""; }; @@ -342,11 +318,6 @@ CDE15FC02ACB497E0036F0A4 /* ReadLessWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadLessWidget.swift; sourceTree = ""; }; CDE15FC22ACB497F0036F0A4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; CDE15FC42ACB497F0036F0A4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - CDE160082ACC8CDB0036F0A4 /* Intents.intentdefinition */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; path = Intents.intentdefinition; sourceTree = ""; }; - CDE1600F2ACDEF770036F0A4 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; - CDE1601A2ACE23950036F0A4 /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; }; - CDE160252ACE23950036F0A4 /* IntentsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IntentsUI.framework; path = System/Library/Frameworks/IntentsUI.framework; sourceTree = SDKROOT; }; - CDE1AC1C2AD04CB800F0E430 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; CDE9EB402A48A3770057217B /* Newsreader72pt-BoldItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Newsreader72pt-BoldItalic.ttf"; sourceTree = ""; }; CDE9EB412A48A3770057217B /* Faustina-Italic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Faustina-Italic.ttf"; sourceTree = ""; }; CDE9EB422A48A3770057217B /* AnekLatin-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "AnekLatin-Regular.ttf"; sourceTree = ""; }; @@ -478,7 +449,7 @@ CDE11B202A786FF500C0BC39 /* PublicPublisherAttributes.swift */, 010F0D2529E88EC40064DD97 /* PublicSummaryAttributes.swift */, 010F0D2129E88EC40064DD97 /* PublicCategoryAttributes.swift */, - 018CE3AF29E98AB9005E6391 /* BulkMetadataResponse.swift */, + 018CE3AF29E98AB9005E6391 /* BulkResponse.swift */, ); path = Models; sourceTree = ""; @@ -527,8 +498,6 @@ 018173BE29EA1842006D3509 /* PressActions.swift */, 018173C229EA4833006D3509 /* Date.swift */, 015A19D029EAEB1A0072EEE3 /* View.swift */, - CDE1600F2ACDEF770036F0A4 /* Image.swift */, - CDE1AC1C2AD04CB800F0E430 /* UIImage.swift */, ); path = Extensions; sourceTree = ""; @@ -536,7 +505,7 @@ 0150ACC229F957E400875CF7 /* Services */ = { isa = PBXGroup; children = ( - 013F32C229E8BA2D00CE8555 /* APIClient.swift */, + 013F32C229E8BA2D00CE8555 /* ConnectService.swift */, ); path = Services; sourceTree = ""; @@ -579,8 +548,6 @@ 19F6CBCC0A4E27FBF8BF4A61 /* libPods-ReadLess-ReadLessTests.a */, 0150ACA429F9577400875CF7 /* WidgetKit.framework */, 0150ACA629F9577400875CF7 /* SwiftUI.framework */, - CDE1601A2ACE23950036F0A4 /* Intents.framework */, - CDE160252ACE23950036F0A4 /* IntentsUI.framework */, ); name = Frameworks; sourceTree = ""; @@ -674,9 +641,6 @@ children = ( CDE15FBC2ACB497E0036F0A4 /* ReadLessWidgetBundle.swift */, CDE15FC02ACB497E0036F0A4 /* ReadLessWidget.swift */, - CDA6AD2F2AD22FD900514CD1 /* Channel.swift */, - CD4BF5A42AD074F7006BDC35 /* WidgetTopicConfiguration.swift */, - CDE160082ACC8CDB0036F0A4 /* Intents.intentdefinition */, CDE15FC22ACB497F0036F0A4 /* Assets.xcassets */, CDE15FC42ACB497F0036F0A4 /* Info.plist */, ); @@ -777,7 +741,6 @@ 01F6EA7829E6FA3C00F51BBA /* Embed Watch Content */, CD6345222A192BF100CB264B /* Embed Foundation Extensions */, 0C61BC585057FDAEA7DDC87A /* [CP-User] [RNFB] Core Configuration */, - CDE160072ACC8C390036F0A4 /* Embed ExtensionKit Extensions */, ); buildRules = ( ); @@ -816,7 +779,7 @@ BuildIndependentTargetsInParallel = YES; CLASSPREFIX = I; LastSwiftUpdateCheck = 1500; - LastUpgradeCheck = 1500; + LastUpgradeCheck = 1430; TargetAttributes = { 00E356ED1AD99517003FC87E = { CreatedOnToolsVersion = 6.2; @@ -934,7 +897,6 @@ CDE9EB5B2A48A3770057217B /* Newsreader72pt-Bold.ttf in Resources */, 0143022E29FACEDC00D40293 /* MaterialIcons.ttf in Resources */, 0143022B29FACEDC00D40293 /* MaterialCommunityIcons.ttf in Resources */, - CDE160132ACE08130036F0A4 /* Images.xcassets in Resources */, CDE9EB4F2A48A3770057217B /* Newsreader72pt-BoldItalic.ttf in Resources */, 0143022529FACEDC00D40293 /* EvilIcons.ttf in Resources */, CDE9EB5D2A48A3770057217B /* AnekLatin-Bold.ttf in Resources */, @@ -1015,7 +977,6 @@ buildActionMask = 2147483647; files = ( CDE15FE12ACB499E0036F0A4 /* Faustina-BoldItalic.ttf in Resources */, - CDE160122ACE08120036F0A4 /* Images.xcassets in Resources */, CDE15FE82ACB499E0036F0A4 /* AnekLatin-Regular.ttf in Resources */, CDE15FC32ACB497F0036F0A4 /* Assets.xcassets in Resources */, CDE15FDB2ACB499E0036F0A4 /* FontAwesome5_Solid.ttf in Resources */, @@ -1266,20 +1227,18 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 018173C129EA4529006D3509 /* SummaryCard.swift in Sources */, 010F0CE629E88D470064DD97 /* ContentView.swift in Sources */, 018173BF29EA1842006D3509 /* PressActions.swift in Sources */, 018173C329EA4833006D3509 /* Date.swift in Sources */, - 013F32C329E8BA2E00CE8555 /* APIClient.swift in Sources */, + 013F32C329E8BA2E00CE8555 /* ConnectService.swift in Sources */, CDE11B212A786FF500C0BC39 /* PublicPublisherAttributes.swift in Sources */, - CDE160102ACDEF770036F0A4 /* Image.swift in Sources */, - 018CE3B029E98AB9005E6391 /* BulkMetadataResponse.swift in Sources */, + 018CE3B029E98AB9005E6391 /* BulkResponse.swift in Sources */, 010F0D8129E89D770064DD97 /* Color.swift in Sources */, 010F0CE429E88D470064DD97 /* ReadLessWatchApp.swift in Sources */, 010F0D5629E88EC40064DD97 /* PublicSummaryAttributes.swift in Sources */, - CDE1AC252AD0585D00F0E430 /* SummaryCard.swift in Sources */, 015A19D129EAEB1A0072EEE3 /* View.swift in Sources */, 010F0D5229E88EC40064DD97 /* PublicCategoryAttributes.swift in Sources */, - CDE1AC1D2AD04CB800F0E430 /* UIImage.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1304,11 +1263,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - CD4BF5A62AD0797C006BDC35 /* WidgetTopicConfiguration.swift in Sources */, 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */, - CDA6AD302AD22FD900514CD1 /* Channel.swift in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, - CD92BEB22AD093940096D6DA /* Intents.intentdefinition in Sources */, 5FFC8B3A3B346CCC17615C08 /* ExpoModulesProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1318,22 +1274,17 @@ buildActionMask = 2147483647; files = ( CDE15FC12ACB497E0036F0A4 /* ReadLessWidget.swift in Sources */, - CDE15FD32ACB49950036F0A4 /* APIClient.swift in Sources */, + CDE15FD32ACB49950036F0A4 /* ConnectService.swift in Sources */, CDE15FD12ACB49920036F0A4 /* View.swift in Sources */, CDE15FCC2ACB498E0036F0A4 /* PublicSummaryAttributes.swift in Sources */, - CDE160112ACDEF770036F0A4 /* Image.swift in Sources */, CDE15FCB2ACB498E0036F0A4 /* PublicPublisherAttributes.swift in Sources */, - CDE15FCD2ACB498E0036F0A4 /* BulkMetadataResponse.swift in Sources */, - CDE160092ACC8CDB0036F0A4 /* Intents.intentdefinition in Sources */, - CDE1AC1E2AD04CB800F0E430 /* UIImage.swift in Sources */, + CDE15FCD2ACB498E0036F0A4 /* BulkResponse.swift in Sources */, CDE15FCF2ACB49920036F0A4 /* Color.swift in Sources */, CDE15FBD2ACB497E0036F0A4 /* ReadLessWidgetBundle.swift in Sources */, CDE15FD22ACB49920036F0A4 /* Date.swift in Sources */, CDE15FD02ACB49920036F0A4 /* PressActions.swift in Sources */, - CDA6AD312AD22FD900514CD1 /* Channel.swift in Sources */, CDE15FCE2ACB498E0036F0A4 /* PublicCategoryAttributes.swift in Sources */, CDE15FD42ACB49970036F0A4 /* SummaryCard.swift in Sources */, - CD4BF5A52AD074F7006BDC35 /* WidgetTopicConfiguration.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1509,7 +1460,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; + MARKETING_VERSION = 1.1.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -1555,7 +1506,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; + MARKETING_VERSION = 1.1.1; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = ai.readless.ios.watchkitapp; @@ -1717,12 +1668,11 @@ INFOPLIST_FILE = ReadLess/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Read Less "; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.magazines-and-newspapers"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.16.3; + MARKETING_VERSION = 1.16.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1731,6 +1681,7 @@ OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = ai.readless.ios; PRODUCT_NAME = ReadLess; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -1758,12 +1709,11 @@ INFOPLIST_FILE = ReadLess/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Read Less "; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.magazines-and-newspapers"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.16.3; + MARKETING_VERSION = 1.16.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1772,6 +1722,7 @@ OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = ai.readless.ios; PRODUCT_NAME = ReadLess; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_VERSION = 5.0; diff --git a/src/mobile/ios/ReadLess.xcodeproj/xcshareddata/xcschemes/ReadLess.xcscheme b/src/mobile/ios/ReadLess.xcodeproj/xcshareddata/xcschemes/ReadLess.xcscheme index 7fc437dbd..972ee7023 100644 --- a/src/mobile/ios/ReadLess.xcodeproj/xcshareddata/xcschemes/ReadLess.xcscheme +++ b/src/mobile/ios/ReadLess.xcodeproj/xcshareddata/xcschemes/ReadLess.xcscheme @@ -1,6 +1,6 @@ - - diff --git a/src/mobile/ios/ReadLess/Info.plist b/src/mobile/ios/ReadLess/Info.plist index bd0581c14..df455b25c 100644 --- a/src/mobile/ios/ReadLess/Info.plist +++ b/src/mobile/ios/ReadLess/Info.plist @@ -74,10 +74,6 @@ $(PRODUCT_NAME) wants to save photos NSPhotoLibraryUsageDescription $(PRODUCT_NAME) wants to access photos - NSUserActivityTypes - - IWidgetTopicConfigurationIntent - UIAppFonts AnekLatin-Regular.ttf @@ -112,7 +108,6 @@ UIBackgroundModes audio - fetch remote-notification UILaunchStoryboardName diff --git a/src/mobile/ios/ReadLessWatch Watch App/Views/ContentView.swift b/src/mobile/ios/ReadLessWatch Watch App/Views/ContentView.swift index ca0ef3cff..db75bf635 100644 --- a/src/mobile/ios/ReadLessWatch Watch App/Views/ContentView.swift +++ b/src/mobile/ios/ReadLessWatch Watch App/Views/ContentView.swift @@ -8,9 +8,9 @@ import SwiftUI struct ContentView: View { - @Environment(\.colorScheme) private var colorScheme - @ObservedObject private var service = APIClient() + @ObservedObject private var service: ConnectService = ConnectService() + @Environment(\.colorScheme) private var colorScheme @State var selectedSummary: PublicSummaryAttributes? var background: Color { @@ -32,11 +32,11 @@ struct ContentView: View { } else { List(self.service.summaries, id: \.id) { summary in NavigationLink( - destination: ScrollView { SummaryCard(summary: summary, style: .small, expanded: true) + destination: ScrollView { SummaryCard(summary: summary, compact: false) }.navigationTitle(summary.translations?["title"] ?? summary.title) , - tag: summary.root, + tag: summary, selection: $selectedSummary) { - SummaryCard(summary: summary, style: .small) + SummaryCard(summary: summary, compact: true) } }.refreshable { self.service.fetchSync() diff --git a/src/mobile/ios/ReadLessWidget/Channel.swift b/src/mobile/ios/ReadLessWidget/Channel.swift deleted file mode 100644 index 9290069cf..000000000 --- a/src/mobile/ios/ReadLessWidget/Channel.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// Enum.swift -// -// -// Created by thom on 10/7/23. -// - -import Foundation -import AppIntents - -@available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *) -enum Channel: String, AppEnum { - case liveFeed - case topStories - case customTopic - - static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Feed Type") - static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [ - .liveFeed: "Live Feed", - .topStories: "Top Stories", - .customTopic: "Custom Topic" - ] - -} - - diff --git a/src/mobile/ios/ReadLessWidget/Info.plist b/src/mobile/ios/ReadLessWidget/Info.plist index e4beea5ad..0f118fb75 100644 --- a/src/mobile/ios/ReadLessWidget/Info.plist +++ b/src/mobile/ios/ReadLessWidget/Info.plist @@ -2,19 +2,6 @@ - NSAppTransportSecurity - - NSExceptionDomains - - readless.ai - - NSExceptionAllowsInsecureHTTPLoads - - NSIncludesSubdomains - - - - NSExtension NSExtensionPointIdentifier diff --git a/src/mobile/ios/ReadLessWidget/Intents.intentdefinition b/src/mobile/ios/ReadLessWidget/Intents.intentdefinition deleted file mode 100644 index 5ed6e85a1..000000000 --- a/src/mobile/ios/ReadLessWidget/Intents.intentdefinition +++ /dev/null @@ -1,281 +0,0 @@ - - - - - INEnums - - - INEnumDisplayName - Channel - INEnumDisplayNameID - j3pgMG - INEnumGeneratesHeader - - INEnumName - Channel - INEnumType - Regular - INEnumValues - - - INEnumValueDisplayName - Live Feed - INEnumValueDisplayNameID - yHsbIT - INEnumValueName - unknown - - - INEnumValueDisplayName - Top Stories - INEnumValueDisplayNameID - jjAaOW - INEnumValueIndex - 1 - INEnumValueName - topStories - - - INEnumValueDisplayName - Custom Topic - INEnumValueDisplayNameID - GHz1XP - INEnumValueIndex - 2 - INEnumValueName - customTopic - - - - - INIntentDefinitionModelVersion - 1.2 - INIntentDefinitionNamespace - 4Ax91W - INIntentDefinitionSystemVersion - 22G91 - INIntentDefinitionToolsBuildVersion - 15A240d - INIntentDefinitionToolsVersion - 15.0 - INIntents - - - INIntentCategory - information - INIntentClassPrefix - I - INIntentDescription - Intent for configuring the topic of a widget - INIntentDescriptionID - vlfd95 - INIntentEligibleForWidgets - - INIntentIneligibleForSuggestions - - INIntentLastParameterTag - 20 - INIntentName - WidgetTopicConfiguration - INIntentParameters - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - Channel - INIntentParameterDisplayNameID - INvacD - INIntentParameterDisplayPriority - 1 - INIntentParameterEnumType - Channel - INIntentParameterEnumTypeNamespace - 4Ax91W - INIntentParameterMetadata - - INIntentParameterMetadataDefaultValue - liveFeed - - INIntentParameterName - channel - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Just to confirm, you wanted ‘${channel}’? - INIntentParameterPromptDialogFormatStringID - natRPv - INIntentParameterPromptDialogType - Confirmation - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - There are ${count} options matching ‘${channel}’. - INIntentParameterPromptDialogFormatStringID - 6pQo3g - INIntentParameterPromptDialogType - DisambiguationIntroduction - - - INIntentParameterTag - 20 - INIntentParameterType - Integer - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - Topic - INIntentParameterDisplayNameID - Q88ETS - INIntentParameterDisplayPriority - 2 - INIntentParameterMetadata - - INIntentParameterMetadataCapitalization - Words - INIntentParameterMetadataDefaultValue - Technology - INIntentParameterMetadataDefaultValueID - 2Lnniv - INIntentParameterMetadataDisableAutocorrect - - INIntentParameterMetadataDisableSmartDashes - - INIntentParameterMetadataDisableSmartQuotes - - - INIntentParameterName - topic - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Topic - INIntentParameterPromptDialogFormatStringID - YAndPU - INIntentParameterPromptDialogType - Primary - - - INIntentParameterRelationship - - INIntentParameterRelationshipParentName - channel - INIntentParameterRelationshipPredicateName - EnumHasExactValue - INIntentParameterRelationshipPredicateValue - customTopic - - INIntentParameterTag - 5 - INIntentParameterType - String - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - Update Interval - INIntentParameterDisplayNameID - AnaQy4 - INIntentParameterDisplayPriority - 3 - INIntentParameterMetadata - - INIntentParameterMetadataDefaultUnit - Seconds - INIntentParameterMetadataDefaultValue - 10 - INIntentParameterMetadataMaximumUnit - Days - INIntentParameterMetadataMaximumValue - 360 - INIntentParameterMetadataMinimumUnit - Seconds - INIntentParameterMetadataMinimumValue - 1 - INIntentParameterMetadataUnit - Minutes - - INIntentParameterName - updateInterval - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterTag - 12 - INIntentParameterType - TimeInterval - - - INIntentResponse - - INIntentResponseCodes - - - INIntentResponseCodeName - success - INIntentResponseCodeSuccess - - - - INIntentResponseCodeName - failure - - - INIntentResponseLastParameterTag - 1 - - INIntentTitle - Widget Topic Configuration - INIntentTitleID - jtISxm - INIntentType - Custom - INIntentVerb - View - - - INTypes - - - diff --git a/src/mobile/ios/ReadLessWidget/ReadLessWidget.swift b/src/mobile/ios/ReadLessWidget/ReadLessWidget.swift index a5a8f4a1c..d32c9dc05 100644 --- a/src/mobile/ios/ReadLessWidget/ReadLessWidget.swift +++ b/src/mobile/ios/ReadLessWidget/ReadLessWidget.swift @@ -6,233 +6,51 @@ // import WidgetKit -import AppIntents -import Intents import SwiftUI -let DEFAULT_TIMELINE_INTERVAL: Double = 10 - -struct CustomWidgetConfiguration { - var channel: Channel = .liveFeed - var topic: String? - var updateInterval: Measurement? -} - struct SummaryEntry: TimelineEntry { - var date: Date = .now - var context: TimelineProviderContext? - var config: CustomWidgetConfiguration? - var summaries = [Summary]() -} - -var WidgetPageSize: Dictionary = [ - .systemSmall: 1, - .systemMedium: 2, - .systemLarge: 5, - .systemExtraLarge: 5, -] - -var WidgetPlaceholders: Dictionary = [ - .systemSmall: [MOCK_SUMMARY_1, MOCK_SUMMARY_2, MOCK_SUMMARY_3], - .systemMedium: [MOCK_SUMMARY_1, MOCK_SUMMARY_2], - .systemLarge: [MOCK_SUMMARY_1, MOCK_SUMMARY_2, MOCK_SUMMARY_3, MOCK_SUMMARY_4], - .systemExtraLarge: [MOCK_SUMMARY_1, MOCK_SUMMARY_2, MOCK_SUMMARY_3, MOCK_SUMMARY_4], -] - -func buildEntries(in context: TimelineProviderContext, - for configuration: CustomWidgetConfiguration) async -> [SummaryEntry] { - let endpoint = configuration.channel == .topStories ? Endpoints.GetTopStories : Endpoints.GetSummaries - let filter = configuration.channel == .topStories ? "" : configuration.channel == .liveFeed ? "" : configuration.topic - let summaries = Array(await APIClient().fetchAsync(endpoint: endpoint, - filter: filter).reversed()) - let pageSize = WidgetPageSize[context.family] ?? 2 - var entries: [SummaryEntry] = [] - for i in stride(from: 0, to: summaries.count, by: pageSize) { - let first = summaries[i] - if context.family != .systemSmall { - await first.loadImagesAsync() - } - var subset = [first] - for j in 1 ..< pageSize { - if let next = i + j < summaries.count ? summaries[i + j] : nil { - if context.family != .systemSmall { - await next.loadImagesAsync() - } - subset.insert(next, at: 0) - } - } - let offset = (configuration.updateInterval?.value ?? DEFAULT_TIMELINE_INTERVAL) * 60 - let fireDate = Date(timeIntervalSinceNow: TimeInterval(floor(Double(i) / Double(pageSize)) * offset)) - let entry = SummaryEntry(date: fireDate, - context: context, - config: configuration, - summaries: subset) - entries.append(entry) - } - return entries + let date: Date + var summary: PublicSummaryAttributes? } -struct Provider: IntentTimelineProvider { +struct Provider: TimelineProvider { + + @Environment(\.colorScheme) private var colorScheme func placeholder(in context: Context) -> SummaryEntry { - return SummaryEntry(context: context, - config: CustomWidgetConfiguration(topic: "Technology"), - summaries: WidgetPlaceholders[context.family] ?? []) + SummaryEntry(date: Date(), summary: nil) } - func getSnapshot(for configuration: IWidgetTopicConfigurationIntent, - in context: Context, - completion: @escaping (SummaryEntry) -> ()) { - let entry = SummaryEntry(context: context, - config: CustomWidgetConfiguration(topic: configuration.topic, - updateInterval: Measurement(value: configuration.updateInterval?.doubleValue ?? DEFAULT_TIMELINE_INTERVAL, - unit: .minutes))) + func getSnapshot(in context: Context, completion: @escaping (SummaryEntry) -> ()) { + let entry = SummaryEntry(date: Date(), summary: nil) completion(entry) } - func getTimeline(for configuration: IWidgetTopicConfigurationIntent, - in context: Context, - completion: @escaping (Timeline) -> Void) { - Task { - let config = CustomWidgetConfiguration(channel: Channel.allCases[configuration.channel.rawValue], - topic: configuration.topic, - updateInterval: Measurement(value: configuration.updateInterval?.doubleValue ?? DEFAULT_TIMELINE_INTERVAL, - unit: .minutes)) - let entries = await buildEntries(in: context, for: config) + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + var entries: [SummaryEntry] = [] + let service = ConnectService() + service.fetchSync() { summaries in + for summary in summaries { + if let date = summary.originalDate { + let entry = SummaryEntry(date: date, summary: summary) + entries.append(entry) + } + } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } } - -} - -@available(iOS 17.0, *) -struct TopicDetail: AppEntity { - - var id: String - let name: String - - static var defaultQuery = TopicQuery() - - static var typeDisplayRepresentation: TypeDisplayRepresentation = "Topic" - - var displayRepresentation: DisplayRepresentation { - DisplayRepresentation(title: "\(name)") - } - - static func allTopics() async -> [TopicDetail] { - return await APIClient().getCategories().map { TopicDetail(id: $0.name, name: $0.name) } - } - -} - -@available(iOS 17, *) -struct TopicQuery: EntityQuery { - func entities(for identifiers: [TopicDetail.ID]) async throws -> [TopicDetail] { - return await TopicDetail.allTopics() - } - - func suggestedEntities() async throws -> [TopicDetail] { - return await TopicDetail.allTopics() - } - - func defaultResult() async -> TopicDetail? { - try? await suggestedEntities().first - } -} - -@available(iOS 17.0, *) -struct AppIntentProvider: AppIntentTimelineProvider { - - typealias Entry = SummaryEntry - typealias Intent = WidgetTopicConfiguration - - func placeholder(in context: Context) -> SummaryEntry { - return SummaryEntry(context: context, - config: CustomWidgetConfiguration(topic: "Technology"), - summaries: WidgetPlaceholders[context.family] ?? []) - } - - func snapshot(for configuration: WidgetTopicConfiguration, - in context: Context) async -> SummaryEntry { - SummaryEntry(context: context, - config: CustomWidgetConfiguration(topic: configuration.topic, - updateInterval: configuration.updateInterval)) - } - - func timeline(for configuration: WidgetTopicConfiguration, - in context: Context) async -> Timeline { - let config = CustomWidgetConfiguration(channel: configuration.channel ?? .liveFeed, - topic: configuration.topic, - updateInterval: configuration.updateInterval) - let entries = await buildEntries(in: context, for: config) - let timeline = Timeline(entries: entries, policy: .atEnd) - return timeline - } - } struct ReadLessWidgetEntryView : View { - - @Environment(\.colorScheme) private var colorScheme - var entry: Provider.Entry - - let iconSize = 20.0 - - var deeplink: URL { - if entry.context?.family == .systemSmall { - return entry.summaries.first?.deeplink ?? URL(string: "https://readless.ai/top")! - } - if entry.config?.channel == .liveFeed { - return URL(string: "https://readless.ai/live")! - } - if entry.config?.channel == .topStories { - return URL(string: "https://readless.ai/top")! - } - return URL(string: "https://readless.ai/search?filter=\(entry.config?.topic ?? "")")! - } var body: some View { - VStack(spacing: 8.0) { - HStack { - Text(entry.config?.channel == .liveFeed ? "Live Feed" : - entry.config?.channel == .topStories ? "Top Stories" : - entry.config?.topic ?? "Topic") - .textCase(.uppercase) - .font(.subheadline) - .bold() - .padding(0) - Spacer() - if colorScheme == .light { - Image("LogoCompact") - .resizable() - .scaledToFit() - .frame(width: iconSize, height: iconSize) - } else { - Image("LogoCompact") - .resizable() - .colorInvert() - .scaledToFit() - .frame(width: iconSize, height: iconSize) - } - } - if (entry.config?.topic == nil) { - ForEach(0 ..< 2) { _ in - SummaryCard(style: entry.context?.family == .systemSmall ? .small : .medium) - } - } else - if (entry.summaries.count == 0) { - Text("No results found") - } else { - ForEach(entry.summaries, id: \.id) { - SummaryCard(summary: $0, - style: entry.context?.family == .systemSmall ? .small : .medium, - deeplink: true) - } + VStack { + if let summary = entry.summary { + SummaryCard(summary: summary, compact: false) } } - .widgetURL(deeplink) } } @@ -240,28 +58,23 @@ struct ReadLessWidget: Widget { let kind: String = "ReadLessWidget" var body: some WidgetConfiguration { - if #available(iOS 17.0, macOS 14.0, watchOS 10.0, *) { - return AppIntentConfiguration(kind: kind, - intent: WidgetTopicConfiguration.self, - provider: AppIntentProvider()) { - ReadLessWidgetEntryView(entry: $0) + StaticConfiguration(kind: kind, provider: Provider()) { entry in + if #available(iOS 17.0, *) { + ReadLessWidgetEntryView(entry: entry) .containerBackground(.fill.tertiary, for: .widget) - } - .configurationDisplayName("Topic") - .description("Choose a topic") - .supportedFamilies([.systemMedium, .systemLarge]) - } else { - return IntentConfiguration(kind: kind, - intent: IWidgetTopicConfigurationIntent.self, - provider: Provider()) { - ReadLessWidgetEntryView(entry: $0) + } else { + ReadLessWidgetEntryView(entry: entry) .padding() .background() } - .configurationDisplayName("Topic") - .description("Choose a topic") - .supportedFamilies([.systemMedium, .systemLarge]) } + .configurationDisplayName("My Widget") + .description("This is an example widget.") } } +#Preview(as: .systemSmall) { + ReadLessWidget() +} timeline: { + SummaryEntry(date: .now, summary: MOCK_SUMMARY) +} diff --git a/src/mobile/ios/ReadLessWidget/WidgetTopicConfiguration.swift b/src/mobile/ios/ReadLessWidget/WidgetTopicConfiguration.swift deleted file mode 100644 index 5c8f6a974..000000000 --- a/src/mobile/ios/ReadLessWidget/WidgetTopicConfiguration.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// WidgetTopicConfiguration.swift -// ReadLess -// -// Created by thom on 10/7/23. -// - -import Foundation -import AppIntents - -@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) -struct WidgetTopicConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMigratedAppIntent, PredictableIntent { - static let intentClassName = "IWidgetTopicConfigurationIntent" - - static var title: LocalizedStringResource = "Widget Topic Configuration" - static var description = IntentDescription("Intent for configuring the topic of a widget") - - @Parameter(title: "Channel", default: .liveFeed) - var channel: Channel? - - @Parameter(title: "Topic", default: "") - var topic: String? - - @Parameter(title: "Update Interval", defaultValue: 10, defaultUnit: .minutes) - var updateInterval: Measurement? - - static var parameterSummary: some ParameterSummary { - When(\.$channel, .equalTo, .customTopic) { - Summary { - \.$channel - \.$topic - \.$updateInterval - } - } otherwise: { - Summary { - \.$channel - \.$updateInterval - } - } - } - - static var predictionConfiguration: some IntentPredictionConfiguration { - IntentPrediction(parameters: (\.$channel, \.$topic, \.$updateInterval)) { feedType, topic, updateInterval in - DisplayRepresentation( - title: "", - subtitle: "" - ) - } - IntentPrediction(parameters: (\.$channel, \.$updateInterval)) { feedType, updateInterval in - DisplayRepresentation( - title: "", - subtitle: "" - ) - } - } - - func perform() async throws -> some IntentResult { - // TODO: Place your refactored intent handler code here. - return .result() - } -} - -@available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *) -fileprivate extension IntentDialog { - static var topicParameterPrompt: Self { - "Topic" - } -} - diff --git a/src/mobile/ios/Services/APIClient.swift b/src/mobile/ios/Services/APIClient.swift deleted file mode 100644 index e51bbd611..000000000 --- a/src/mobile/ios/Services/APIClient.swift +++ /dev/null @@ -1,116 +0,0 @@ -// -// ConnectService.swift -// ReadLessWatch Watch App -// -// Created by tmorgan on 4/13/23. -// - -import SwiftUI - -struct Endpoints { - static let Root = "https://api.readless.ai/v1" - static let GetSummaries = "\(Root)/summary" - static let GetTopStories = "\(Root)/summary/top" - static let GetCategories = "\(Root)/category" -} - -func parseQuery(endpoint: String = Endpoints.GetSummaries, filter: String?) -> URL? { - guard let filter = filter?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) - else { return URL(string: endpoint) } - return URL(string: endpoint + "?filter=\(filter)") -} - -class APIClient: ObservableObject { - @Published var summaries = [Summary]() - @Published var loading = false - @Published var error: String? - - var decoder: JSONDecoder { - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .custom { (decoder) -> Date in - let container = try decoder.singleValueContainer() - let dateString = try container.decode(String.self) - let dateFormatter = DateFormatter() - dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" - if let date = dateFormatter.date(from: dateString) { - return date - } - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" - if let date = dateFormatter.date(from: dateString) { - return date - } - throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date format") - } - return decoder - } - - init(summaries: [Summary] = [Summary]()) { - self.summaries = summaries - } - - func fetchHandler(_ data: Data?) -> [Summary] { - if let data = data { - do { - let decodedResponse = try decoder.decode(BulkResponse.self, from: data) - self.summaries = decodedResponse.rows.map { Summary($0) } - } catch { - print(error) - self.error = error.localizedDescription - } - } - self.loading = false - return self.summaries - } - - @Sendable func fetchSync() { - return self.fetchSync(nil) - } - - @Sendable func fetchSync(endpoint: String = Endpoints.GetSummaries, filter: String? = "", _ callback: ((_ summaries: [Summary]) -> ())?) { - guard let url = parseQuery(endpoint: endpoint, filter: filter) else { - return - } - loading = true - error = nil - let request = URLRequest(url: url) - URLSession.shared.dataTask(with: request) { (data, _, _) in - DispatchQueue.main.async { - let summaries = self.fetchHandler(data) - callback?(summaries) - } - }.resume() - } - - @Sendable func fetchAsync(endpoint: String = Endpoints.GetSummaries, filter: String? = "") async -> [Summary] { - guard let url = parseQuery(endpoint: endpoint, filter: filter) else { - return [] - } - loading = true - error = nil - let request = URLRequest(url: url) - guard let (data, _) = try? await URLSession.shared.data(for: request) else { - return [] - } - return self.fetchHandler(data) - } - - @Sendable func getCategories() async -> [PublicCategoryAttributes] { - guard let url = URL(string: Endpoints.GetCategories) else { return [] } - print("fetching categories") - let request = URLRequest(url: url) - guard let (data, _) = try? await URLSession.shared.data(for: request) else { - return [] - } - print("fetched categories") - do { - let decodedResponse = try decoder.decode(BulkResponse.self, from: data) - print(decodedResponse.rows) - return decodedResponse.rows - } catch { - print(error) - } - return [] - } - -} diff --git a/src/mobile/ios/Services/ConnectService.swift b/src/mobile/ios/Services/ConnectService.swift new file mode 100644 index 000000000..71f47d86d --- /dev/null +++ b/src/mobile/ios/Services/ConnectService.swift @@ -0,0 +1,85 @@ +// +// ConnectService.swift +// ReadLessWatch Watch App +// +// Created by tmorgan on 4/13/23. +// + +import SwiftUI + +struct Endpoints { + static let GetSummaries = "https://api.readless.ai/v1/summary" +} + +class ConnectService: ObservableObject { + @Published var summaries = [PublicSummaryAttributes]() + @Published var loading = false + @Published var error: String? + + init(summaries: [PublicSummaryAttributes] = [PublicSummaryAttributes]()) { + self.summaries = summaries + } + + func fetchHandler(_ data: Data?) -> [PublicSummaryAttributes] { + if let data = data { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .custom { (decoder) -> Date in + let container = try decoder.singleValueContainer() + let dateString = try container.decode(String.self) + let dateFormatter = DateFormatter() + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + if let date = dateFormatter.date(from: dateString) { + return date + } + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" + if let date = dateFormatter.date(from: dateString) { + return date + } + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date format") + } + do { + let decodedResponse = try decoder.decode(BulkResponse.self, from: data) + self.summaries = decodedResponse.rows + } catch { + print(error) + self.error = error.localizedDescription + } + } + self.loading = false + return self.summaries + } + + @Sendable func fetchSync() { + return self.fetchSync(nil) + } + + @Sendable func fetchSync(_ callback: ((_ summaries: [PublicSummaryAttributes]) -> ())?) { + guard let url = URL(string: Endpoints.GetSummaries) else { + return + } + loading = true + error = nil + let request = URLRequest(url: url) + URLSession.shared.dataTask(with: request) { (data, _, _) in + DispatchQueue.main.async { + let summaries = self.fetchHandler(data) + callback?(summaries) + } + }.resume() + } + + @Sendable func fetchAsync() async -> [PublicSummaryAttributes] { + guard let url = URL(string: Endpoints.GetSummaries) else { + return [] + } + loading = true + error = nil + let request = URLRequest(url: url) + guard let (data, _) = try? await URLSession.shared.data(for: request) else { + return [] + } + return self.fetchHandler(data) + } + +} diff --git a/src/mobile/ios/Views/SummaryCard.swift b/src/mobile/ios/Views/SummaryCard.swift index 45f74335c..c1d8629d2 100644 --- a/src/mobile/ios/Views/SummaryCard.swift +++ b/src/mobile/ios/Views/SummaryCard.swift @@ -7,201 +7,88 @@ import SwiftUI -enum SummaryCardStyle { - case small - case medium -} - struct SummaryCard: View { - var summary: Summary? - let style: SummaryCardStyle - var expanded: Bool = false - var deeplink: Bool = false - - @State var image: Image? = nil - @State var publisherIcon: Image? = nil + let summary: PublicSummaryAttributes + let compact: Bool + @State private var image: Image? = nil + @State private var publisherIcon: Image? = nil @Environment(\.colorScheme) private var colorScheme - var backdrop: Color { - return colorScheme == .light ? Color(hex: 0xffffff, alpha: 0.3) : Color(hex: 0x000000, alpha: 0.3) + var card: Color { + return colorScheme == .light ? Color(hex: 0xEEEEEE) : Color(hex: 0x111111) } - var placeholder: Color { - return colorScheme == .light ? Color(hex: 0xf0f0f0) : Color(hex: 0x303030) - } - - var headerHeight: CGFloat { - return 15.0 - } - - var headerFont: Font { - return .system(size: 10) - } - - var titleFont: Font { - return .caption - } - - var imageHeight: CGFloat { - return style == .small ? 170.0 : 55.0 - } - - var HEADER: some View { - HStack { - if let summary = summary { - if let image = summary.publisherIcon ?? publisherIcon { - image - .resizable() - .frame(width: headerHeight, height: headerHeight) - .aspectRatio(contentMode: .fit) - } else { - Text("") - .frame(width: headerHeight, height: headerHeight) - .onAppear { - Image.load(from: summary.publisher.icon) { image in DispatchQueue.main.async { self.publisherIcon = image } } - } + var primary = Color(hex: 0x8B0000) + + var body: some View { + VStack(spacing: 1) { + VStack(alignment: .leading) { + HStack { + if let image = publisherIcon { + image + .fixedSize() + .frame(width: 20, height: 20) + .aspectRatio(contentMode: .fit) + } else { + Text("") + .onAppear { + loadImage(summary.publisher.icon) { image in self.publisherIcon = image } + } + } + Text(summary.publisher.displayName) + .frame(maxWidth: .infinity) + .padding(3) } - Text(summary.publisher.displayName) - .font(headerFont) - Text("•") - .font(headerFont) + .background(self.primary) + .cornerRadius(8) + .frame(maxWidth: .infinity) + .multilineTextAlignment(.leading) + Text(summary.category.displayName) + .frame(maxWidth: .infinity) + .padding(3) Text(summary.originalDate?.distanceFromNow() ?? "") - .font(headerFont) - Spacer() - } - } - .multilineTextAlignment(.leading) - } - - var TITLE: some View { - HStack { - if let summary = summary { - Text(summary.translations?["title"] ?? summary.title) - .font(titleFont) - .bold() - .lineSpacing(-10.0) - .truncationMode(.tail) - .lineLimit(!expanded ? 2 : 10) - } - Spacer() - } - .multilineTextAlignment(.leading) - } - - var DESCRIPTION: some View { - HStack { - if let summary = summary { - Text(summary.translations?["shortSummary"] ?? summary.shortSummary ?? "") - .font(titleFont) - .bold() - .lineSpacing(-10.0) - } - } - .multilineTextAlignment(.leading) - } - - var IMAGE: some View { - VStack { - if let summary = summary { - if let image = summary.image ?? image { + .font(.footnote) + .frame(maxWidth: .infinity) + .padding(3) + .multilineTextAlignment(.leading) + Divider() + if let image = image { image - .resizable() - .scaledToFill() - .frame(width: imageHeight, height: imageHeight) + .resizable() + .aspectRatio(contentMode: .fit) } else { - Text("Loading Image...") - .font(headerFont) - .frame(width: imageHeight, height: imageHeight) + Text("Loading Image...") .onAppear { - if let url = summary.primaryImageUrl { - Image.load(from: url) { image in DispatchQueue.main.async { self.image = image } } - } - } - } - } - } - } - - var CONTENT: some View { - VStack { - if style == .small { - if !expanded { - IMAGE - .overlay { - VStack { - Spacer() - VStack { - HEADER - TITLE - } - .padding(4.0) - .cornerRadius(8.0) - .background(backdrop) + if let url = summary.media?["imageArticle"] ?? summary.media?["imageAi1"] ?? summary.imageUrl { + loadImage(url) { image in self.image = image } } } - .padding(4.0) - } else { - VStack(spacing: 8.0) { - HEADER - TITLE - IMAGE - DESCRIPTION - } } - } else - if style == .medium { - HStack { - VStack(spacing: 4.0) { - HEADER - TITLE + Text(compact ? (summary.translations?["title"] ?? summary.title) : (summary.translations?["shortSummary"] ?? summary.shortSummary ?? "")) + .padding(3) + .if(compact) { view in + view + .truncationMode(.tail) + .lineLimit(compact ? 3 : 10) } - .multilineTextAlignment(.leading) - IMAGE - .cornerRadius(8.0) - } - .frame(maxWidth: .infinity) } + .frame(maxWidth: .infinity) + .padding(10) } + .background(self.card) + .cornerRadius(8) } + +} - var body: some View { - if let summary = summary { - if deeplink == true { - Link(destination: summary.deeplink, label: { - CONTENT - }) - } else { - CONTENT - } - } else { - if style == .small { - - } else - if style == .medium { - HStack { - VStack(spacing: 4.0) { - HStack { - Spacer() - } - .frame(height: headerHeight) - .background(placeholder) - HStack { - Spacer() +func loadImage(_ url: String, completionHandler: @escaping @Sendable (_ image: Image) -> Void) { + guard let imageUrl = URL(string: url) else { return } + URLSession.shared.dataTask(with: imageUrl) { data, _, error in + if let data = data, let uiImage = UIImage(data: data) { + DispatchQueue.main.async { + completionHandler(Image(uiImage: uiImage)) } - .frame(height: headerHeight) - .background(placeholder) - } - VStack { - Spacer() - } - .frame(width: imageHeight, height: imageHeight) - .background(placeholder) - .cornerRadius(8.0) } - .frame(maxWidth: .infinity) - } - } - } - + }.resume() } diff --git a/src/mobile/src/LeftDrawerScreen.tsx b/src/mobile/src/LeftDrawerScreen.tsx index e4432ff3b..3ae93e3f9 100644 --- a/src/mobile/src/LeftDrawerScreen.tsx +++ b/src/mobile/src/LeftDrawerScreen.tsx @@ -14,7 +14,7 @@ import { DrawerItem, DrawerSection, Icon, - RoutedScreen, + Screen, View, } from '~/components'; import { SessionContext } from '~/contexts'; @@ -23,9 +23,9 @@ import { strings } from '~/locales'; function HomeDrawer() { return ( - - - + + + ); } diff --git a/src/mobile/src/NavigationController.tsx b/src/mobile/src/NavigationController.tsx index e52ffe593..1d998ebbb 100644 --- a/src/mobile/src/NavigationController.tsx +++ b/src/mobile/src/NavigationController.tsx @@ -169,7 +169,9 @@ export default function NavigationController() { linking={ NAVIGATION_LINKING_OPTIONS }> - + + + diff --git a/src/mobile/src/RightDrawerScreen.tsx b/src/mobile/src/RightDrawerScreen.tsx index bdc8c96dd..b0efa1dfe 100644 --- a/src/mobile/src/RightDrawerScreen.tsx +++ b/src/mobile/src/RightDrawerScreen.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Linking } from 'react-native'; import { DrawerContentComponentProps, @@ -61,6 +62,24 @@ function RightDrawerContent(props: DrawerContentComponentProps) { const RightDrawer = createDrawerNavigator(); export function RightDrawerScreen() { + + const { router } = useNavigation(); + + const [loadedInitialUrl, setLoadedInitialUrl] = React.useState(false); + + React.useEffect(() => { + const subscriber = Linking.addEventListener('url', router); + if (!loadedInitialUrl) { + Linking.getInitialURL().then((url) => { + if (url) { + router({ url } ); + setLoadedInitialUrl(true); + } + }); + } + return () => subscriber.remove(); + }, [router, loadedInitialUrl]); + return ( + {STACK_SCREENS.map((screen) => ( { - const subscriber = Linking.addEventListener('url', router); - if (!loadedInitialUrl) { - Linking.getInitialURL().then((url) => { - if (url) { - setPreference('loadedInitialUrl', true); - router({ navigator: navigation?.getParent('Stack'), url }); - } - }); - } - return () => subscriber.remove(); - }, [router, navigation, loadedInitialUrl, setPreference])); - - return ( - - ); -} \ No newline at end of file diff --git a/src/mobile/src/components/common/Screen.tsx b/src/mobile/src/components/common/Screen.tsx index 173eaf273..083d94614 100644 --- a/src/mobile/src/components/common/Screen.tsx +++ b/src/mobile/src/components/common/Screen.tsx @@ -1,14 +1,11 @@ import React from 'react'; -import { - SafeAreaView, - StatusBar, - StyleSheet, -} from 'react-native'; +import { StyleSheet } from 'react-native'; +import { SafeAreaView, StatusBar } from 'react-native'; import { View, ViewProps } from '~/components'; import { useTheme } from '~/hooks'; -export type ScreenProps = ViewProps & { +export type ScreenViewProps = ViewProps & { safeArea?: boolean; }; diff --git a/src/mobile/src/components/common/index.ts b/src/mobile/src/components/common/index.ts index b2e5ed4c9..9d061c203 100644 --- a/src/mobile/src/components/common/index.ts +++ b/src/mobile/src/components/common/index.ts @@ -25,7 +25,6 @@ export * from './Markdown'; export * from './Popover'; export * from './Pulse'; export * from './MeterDial'; -export * from './RoutedScreen'; export * from './Screen'; export * from './ScrollView'; export * from './SearchMenu'; diff --git a/src/mobile/src/hooks/useNavigation.tsx b/src/mobile/src/hooks/useNavigation.tsx index 931891252..e8c291d71 100644 --- a/src/mobile/src/hooks/useNavigation.tsx +++ b/src/mobile/src/hooks/useNavigation.tsx @@ -16,30 +16,30 @@ export function useNavigation() { const { emitEvent } = usePlatformTools(); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const navigation = useRNNavigation>(); + const navigation = useRNNavigation>() as any; const { preferredReadingFormat, setPreference } = React.useContext(SessionContext); - const navigate = React.useCallback((route: R, params?: RoutingParams[R], navigator?: any) => { + const navigate = React.useCallback((route: R, params?: RoutingParams[R]) => { emitEvent('navigate', route); // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (navigator?.push ?? (navigation as any).push ?? navigation.navigate)(route, params as RoutingParams[R]); + return ((navigation as any).push ?? navigation.navigate)(route, params as RoutingParams[R]); }, [emitEvent, navigation]); - const search = React.useCallback((params: RoutingParams['search'], navigator?: any) => { + const search = React.useCallback((params: RoutingParams['search']) => { const prefilter = params.prefilter; if (!prefilter) { return; } setPreference('searchHistory', (prev) => Array.from(new Set([prefilter, ...(prev ?? [])])).slice(0, 10)); - navigate('search', params, navigator); + navigate('search', params); }, [navigate, setPreference]); - const openSummary = React.useCallback((props: RoutingParams['summary'], navigator?: any) => { + const openSummary = React.useCallback((props: RoutingParams['summary']) => { navigate('summary', { ...props, initialFormat: props.initialFormat ?? preferredReadingFormat ?? ReadingFormat.Bullets, - }, navigator); + }); }, [navigate, preferredReadingFormat]); const openPublisher = React.useCallback((publisher: PublicPublisherAttributes) => { @@ -50,7 +50,7 @@ export function useNavigation() { navigate('category', { category }); }, [navigate]); - const router = React.useCallback(({ url, navigator }: { url: string, navigator?: any }) => { + const router = React.useCallback(({ url }: { url: string }) => { // http://localhost:6969/read/?s=158&f=casual // https://dev.readless.ai/read/?s=158&f=casual // https://www.readless.ai/read/?s=4070&f=bullets @@ -65,39 +65,15 @@ export function useNavigation() { params[decodeURIComponent(key)] = decodeURIComponent(value || ''); }); } - if (route === 'read') { - const summary = Number.parseInt(params['s'] ?? '0'); - if (!summary) { - return; - } - const initialFormat = readingFormat(params['f']); - openSummary({ initialFormat, summary }, navigator); - } else - if (route === 'top') { - navigate('topStories'); - } else - if (route === 'search') { - const filter = params['filter']?.trim(); - if (!filter) { - return; - } - search({ prefilter: filter }, navigator); - } else - if (route === 'publisher') { - const publisher = params['publisher']?.trim(); - if (!publisher) { - return; - } - openPublisher({ name: publisher }); - } else - if (route === 'category') { - const category = params['category']?.trim(); - if (!category) { - return; - } - openCategory({ name: category }); - } - }, [navigate, navigation, search, openSummary, openPublisher, openCategory]); + const summary = Number.parseInt(params['s'] ?? '0'); + if (!summary) { + return; + } + const initialFormat = readingFormat(params['f']); + if (route === 'read' && summary) { + openSummary({ initialFormat, summary }); + } + }, [openSummary]); return { navigate, diff --git a/src/mobile/src/screens/RecapScreen.tsx b/src/mobile/src/screens/RecapScreen.tsx index fdeb472a2..2b001a30b 100644 --- a/src/mobile/src/screens/RecapScreen.tsx +++ b/src/mobile/src/screens/RecapScreen.tsx @@ -1,17 +1,21 @@ import React from 'react'; -import { Recap, RoutedScreen } from '~/components'; +import { Recap, Screen } from '~/components'; +import { getLocale } from '~/locales'; import { ScreenProps } from '~/screens'; -export function RecapScreen({ route }: ScreenProps<'recap'>) { +export function RecapScreen({ + route, + navigation, +}: ScreenProps<'recap'>) { const recap = React.useMemo(() => route?.params?.recap, [route]); return ( - + {recap && ( )} - + ); } \ No newline at end of file diff --git a/src/mobile/src/screens/SearchScreen.tsx b/src/mobile/src/screens/SearchScreen.tsx index 3a1d0d191..5ec4312f0 100644 --- a/src/mobile/src/screens/SearchScreen.tsx +++ b/src/mobile/src/screens/SearchScreen.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { RoutedScreen, SummaryList } from '~/components'; +import { Screen, SummaryList } from '~/components'; import { useApiClient } from '~/hooks'; import { ScreenProps } from '~/screens'; @@ -11,13 +11,13 @@ export function SearchScreen({ const { getSummaries } = useApiClient(); return ( - + - + ); } \ No newline at end of file diff --git a/src/mobile/src/screens/SummaryScreen.tsx b/src/mobile/src/screens/SummaryScreen.tsx index 691bb3d4e..62a90f5b2 100644 --- a/src/mobile/src/screens/SummaryScreen.tsx +++ b/src/mobile/src/screens/SummaryScreen.tsx @@ -11,7 +11,7 @@ import { import { ActivityIndicator, FlatList, - RoutedScreen, + Screen, Summary, Text, View, @@ -106,7 +106,7 @@ export function SummaryScreen({ }, [summary, format, navigation])); return ( - + {loading ? ( @@ -147,6 +147,6 @@ export function SummaryScreen({ ListFooterComponentStyle={ { paddingBottom: 64 } } estimatedItemSize={ 114 } /> ))} - + ); } diff --git a/src/server/src/api/v1/schema/resources/summary/queries/search_conservative.sql b/src/server/src/api/v1/schema/resources/summary/queries/search_conservative.sql deleted file mode 100644 index e8728eb37..000000000 --- a/src/server/src/api/v1/schema/resources/summary/queries/search_conservative.sql +++ /dev/null @@ -1,154 +0,0 @@ -SELECT - "totalCount"::INT AS "count", - JSON_BUILD_OBJECT( - 'sentiment', "averageSentiment" - ) AS metadata, - JSON_AGG(c.*) AS rows -FROM ( - SELECT - s.id, - s.url, - b."originalDate", - s."createdAt", - s.title, - s."shortSummary", - s.summary, - s.bullets, - s."imageUrl", - JSON_BUILD_OBJECT( - 'id', pub.id, - 'name', pub.name, - 'displayName', pub."displayName" - ) AS publisher, - JSON_BUILD_OBJECT( - 'id', cat.id, - 'name', cat.name, - 'displayName', cat."displayName", - 'icon', cat.icon - ) AS category, - ss.sentiment, - ss.sentiments::JSONB AS sentiments, - sm.media::JSONB AS media, - st.translations::JSONB AS translations, - COALESCE( - JSON_AGG(sr."siblingId") - FILTER (WHERE sr."siblingId" IS NOT NULL), - '[]'::JSON) AS siblings, - "averageSentiment", - "totalCount" - FROM ( - SELECT - *, - AVG(sentiment) OVER() AS "averageSentiment", - COUNT(id) OVER() AS "totalCount" - FROM ( - SELECT - s.id, - s."originalDate", - ss.sentiment - FROM - summaries s - LEFT OUTER JOIN categories cat ON s."categoryId" = cat.id - LEFT OUTER JOIN publishers pub ON s."publisherId" = pub.id - LEFT OUTER JOIN summary_translations st ON st."parentId" = s.id - AND st.locale = :locale - LEFT OUTER JOIN summary_sentiment_view ss ON ss."parentId" = s.id - WHERE - s."deletedAt" IS NULL - AND ( - (s."originalDate" > NOW() - INTERVAL :interval) - OR ( - (s."originalDate" >= :startDate) - AND (s."originalDate" <= :endDate) - ) - ) - AND ( - (s.id IN (:ids)) - OR :noIds - ) - AND ( - (:excludeIds AND s.id NOT IN (:ids)) - OR NOT :excludeIds - ) - AND ( - (pub.name IN (:publishers)) - OR :noPublishers - ) - AND ( - (pub.name NOT IN (:excludedPublishers)) - OR :noExcludedPublishers - ) - AND ( - (cat.name IN (:categories)) - OR :noCategories - ) - AND ( - (cat.name NOT IN (:excludedCategories)) - OR :noExcludedCategories - ) - AND ( - :noFilter - OR (s.title ~* :filter) - OR (s."shortSummary" ~* :filter) - OR (s.summary ~* :filter) - OR (s.bullets::TEXT ~* :filter) - OR (st.value ~* :filter) - ) - ORDER BY - s."originalDate" DESC - ) a - ORDER BY - a."originalDate" DESC - LIMIT :limit - OFFSET :offset - ) b - LEFT OUTER JOIN summaries s - ON b.id = s.id - AND (s."deletedAt" IS NULL) - LEFT OUTER JOIN publisher_view pub - ON s."publisherId" = pub.id - AND (pub.locale = :locale OR pub.locale IS NULL) - LEFT OUTER JOIN category_view cat - ON s."categoryId" = cat.id - AND (cat.locale = :locale OR cat.locale IS NULL) - LEFT OUTER JOIN summary_sentiment_view ss - ON ss."parentId" = s.id - LEFT OUTER JOIN summary_media_view sm - ON sm."parentId" = s.id - LEFT OUTER JOIN summary_translation_view st - ON st."parentId" = s.id - AND st.locale = :locale - LEFT OUTER JOIN summary_relations sr - ON b.id = sr."parentId" - GROUP BY - s.id, - s.url, - b."originalDate", - s."createdAt", - s.title, - s."shortSummary", - s.summary, - s.bullets, - s."imageUrl", - pub.id, - pub.name, - pub."displayName", - pub.description, - pub.translations::JSONB, - cat.id, - cat.name, - cat."displayName", - cat.icon, - cat.translations::JSONB, - ss.sentiment, - ss.sentiments::JSONB, - sm.media::JSONB, - st.translations::JSONB, - "averageSentiment", - "totalCount" - ORDER BY - b."originalDate" DESC -) c -GROUP BY - "averageSentiment", - "totalCount"; \ No newline at end of file diff --git a/src/server/src/api/v1/schema/resources/summary/queries/top_stories_conservative.sql b/src/server/src/api/v1/schema/resources/summary/queries/top_stories_conservative.sql deleted file mode 100644 index 395fed0f9..000000000 --- a/src/server/src/api/v1/schema/resources/summary/queries/top_stories_conservative.sql +++ /dev/null @@ -1,156 +0,0 @@ -SELECT "totalCount"::INT AS "count", - JSON_AGG(c.*) AS rows -FROM ( - SELECT - b.id, - s."title", - s."shortSummary", - s.summary, - s.bullets, - s.url, - s."originalDate", - s."imageUrl", - JSON_BUILD_OBJECT( - 'id', pub.id, - 'name', pub.name, - 'displayName', pub."displayName" - ) AS publisher, - JSON_BUILD_OBJECT( - 'id', cat.id, - 'name', cat.name, - 'displayName', cat."displayName", - 'icon', cat.icon - ) AS category, - ss.sentiment, - ss.sentiments::JSONB AS sentiments, - sm.media::JSONB AS media, - st.translations::JSONB AS translations, - "siblingCount", - COALESCE( - JSON_AGG(sr."siblingId") - FILTER (WHERE sr."siblingId" IS NOT NULL), - '[]'::JSON) AS siblings, - "totalCount" - FROM ( - SELECT - *, - COUNT(a.id) OVER() AS "totalCount" - FROM ( - SELECT - s.id, - s."originalDate", - COUNT(sibling.id) AS "siblingCount" - FROM summaries s - LEFT OUTER JOIN categories cat ON s."categoryId" = cat.id - LEFT OUTER JOIN publishers pub ON s."publisherId" = pub.id - LEFT OUTER JOIN summary_translations st ON st."parentId" = s.id - AND st.locale = :locale - LEFT OUTER JOIN "summary_relations" sr ON (s.id = sr."parentId") - AND (sr."deletedAt" IS NULL) - LEFT OUTER JOIN summaries sibling ON (sibling.id = sr."siblingId") - AND (sibling."deletedAt" IS NULL) - AND (sibling."originalDate" > NOW() - INTERVAL :interval) - WHERE - s."deletedAt" IS NULL - AND ( - (s."originalDate" > NOW() - INTERVAL :interval) - OR ( - (s."originalDate" >= :startDate) - AND (s."originalDate" <= :endDate) - ) - ) - AND ( - (s.id IN (:ids)) - OR :noIds - ) - AND ( - (:excludeIds AND s.id NOT IN (:ids)) - OR NOT :excludeIds - ) - AND ( - (pub.name IN (:publishers)) - OR :noPublishers - ) - AND ( - (pub.name NOT IN (:excludedPublishers)) - OR :noExcludedPublishers - ) - AND ( - (cat.name IN (:categories)) - OR :noCategories - ) - AND ( - (cat.name NOT IN (:excludedCategories)) - OR :noExcludedCategories - ) - AND ( - :noFilter - OR (s.title ~* :filter) - OR (s."shortSummary" ~* :filter) - OR (s.summary ~* :filter) - OR (s.bullets::TEXT ~* :filter) - OR (st.value ~* :filter) - ) - GROUP BY - s.id, - s."originalDate" - ORDER BY - "siblingCount" DESC, - s."originalDate" DESC - ) a - ORDER BY - a."siblingCount" DESC, - a."originalDate" DESC - LIMIT :limit - OFFSET :offset - ) b - LEFT OUTER JOIN summaries s - ON b.id = s.id - AND (s."deletedAt" IS NULL) - LEFT OUTER JOIN publisher_view pub - ON s."publisherId" = pub.id - AND (pub.locale = :locale OR pub.locale IS NULL) - LEFT OUTER JOIN category_view cat - ON s."categoryId" = cat.id - AND (cat.locale = :locale OR cat.locale IS NULL) - LEFT OUTER JOIN summary_sentiment_view ss - ON ss."parentId" = s.id - LEFT OUTER JOIN summary_media_view sm - ON sm."parentId" = s.id - LEFT OUTER JOIN summary_translation_view st - ON st."parentId" = s.id - AND st.locale = :locale - LEFT OUTER JOIN summary_relations sr - ON b.id = sr."parentId" - GROUP BY - b.id, - s.url, - s."originalDate", - s."createdAt", - s.title, - s."shortSummary", - s.summary, - s.bullets, - s."imageUrl", - pub.id, - pub.name, - pub."displayName", - pub.description, - pub.translations::JSONB, - cat.id, - cat.name, - cat."displayName", - cat.icon, - cat.translations::JSONB, - ss.sentiment, - ss.sentiments::JSONB, - sm.media::JSONB, - st.translations::JSONB, - "siblingCount", - "totalCount" - ORDER BY - b."siblingCount" DESC, - s."originalDate" DESC -) c -GROUP BY - "totalCount"; \ No newline at end of file diff --git a/src/server/src/services/puppeteer/PuppeteerService.ts b/src/server/src/services/puppeteer/PuppeteerService.ts index f3183ee1c..f00f3402a 100644 --- a/src/server/src/services/puppeteer/PuppeteerService.ts +++ b/src/server/src/services/puppeteer/PuppeteerService.ts @@ -1,5 +1,4 @@ import axios from 'axios'; -import { load } from 'cheerio'; import ms from 'ms'; import puppeteer, { Browser, @@ -170,8 +169,6 @@ export class PuppeteerService extends BaseService { waitUntil = 'domcontentloaded', }: PageOptions = {} ): Promise { - - console.log(`loaded ${actions.length} actions`); let browser: Browser; try { @@ -195,7 +192,6 @@ export class PuppeteerService extends BaseService { } for (const selectorAction of actions) { - console.log(`running action "${selectorAction?.selector}"`); try { const { selector, firstMatchOnly, pageOptions, action, @@ -340,14 +336,7 @@ export class PuppeteerService extends BaseService { public static async loot( url: string, publisher: PublisherCreationAttributes, - { - content, exclude = [ - 'img', - 'script', - 'source', - 'style', - ], - }: LootOptions = {} + { content }: LootOptions = {} ): Promise { const loot: Loot = { @@ -368,102 +357,6 @@ export class PuppeteerService extends BaseService { const dates: string[] = []; const imageUrls: string[] = []; const actions: SelectorAction[] = []; - - if (!content) { - - const { - article, author, date, title, image, - } = publisher.selectors; - - const rawHtml = await PuppeteerService.fetch(url); - - const authors: string[] = []; - const dates: string[] = []; - const imageUrls: string[] = []; - - if (rawHtml) { - - loot.rawText = rawHtml; - const $ = load(rawHtml); - - const nextData = $('script#__NEXT_DATA__').text(); - if (nextData) { - try { - console.log(nextData.substring(0, 500)); - const match = nextData.match(/"date"[\s\n]*:[\s\n]*"(.*?)"/m); - console.log(match); - if (match) { - dates.push(match[1]); - } - } catch (e) { - console.error(e); - } - } - - const extract = ( - sel: string, - attr?: string, - first?: boolean - ): string => { - if (attr && clean($(sel)?.attr(attr))) { - if (first) { - return clean($(sel)?.first()?.attr(attr)); - } - return clean($(sel).attr(attr)); - } - if (first) { - return clean($(sel)?.first()?.text()); - } - return $(sel)?.map((i, el) => clean($(el).text())).get().filter(Boolean).join(' '); - }; - - const extractAll = (sel: string, attr?: string): string[] => { - return $(sel)?.map((i, el) => clean(attr ? $(el).attr(attr) : $(el).text())).get().filter(Boolean) ?? []; - }; - - // image - for (const selector of [image?.selector, ...SELECTORS.image].filter(Boolean)) { - for (const attr of [image?.attribute, ...ATTRIBUTES.image].filter(Boolean)) { - imageUrls.push(...extractAll(selector, attr).flatMap((src) => parseSrcset(src, { publisher, targetUrl: url }))); - } - } - - exclude.forEach((tag) => $(tag).remove()); - - // title - loot.title = extract(title?.selector || 'title', title?.attribute); - // content - loot.content = article ? extract(article.selector, article.attribute) || extract('h1,h2,h3,h4,h5,h6,p,blockquote') : extract('h1,h2,h3,h4,h5,h6,p,blockquote'); - - // dates - - dates.push( - ...extractAll(date.selector), - ...extractAll(date.selector, 'datetime') - ); - if (date.attribute) { - dates.push( - ...extractAll(date.selector, date.attribute), - extract(date.selector, date.attribute) - ); - } - dates.push( - extract(date.selector), - extract(date.selector, 'datetime') - ); - - loot.date = maxDate(...dates); - if (!loot.date || Number.isNaN(loot.date.valueOf())) { - loot.date = parseDate(dates.join(' ')); - } - - loot.imageUrls = Array.from(new Set(imageUrls.filter((url) => url && !/\.(gif|svg)/i.test(url)))).slice(0, MAX_IMAGE_COUNT); - - // authors - authors.push(...$(author.selector || 'author').map((i, el) => $(el).text()).get()); - } - - } // content if (!loot.content) { @@ -518,57 +411,49 @@ export class PuppeteerService extends BaseService { } // dates - if (!loot.date) { - const dateSelectors = [date.selector, 'article time']; - if (date.firstOnly) { - dateSelectors.push(SELECTORS.article); - } - for (const selector of dateSelectors) { - actions.push({ - action: async (el) => { + const dateSelectors = [date.selector, 'article time']; + if (date.firstOnly) { + dateSelectors.push(SELECTORS.article); + } + for (const selector of dateSelectors) { + actions.push({ + action: async (el) => { + dates.push( + await el.evaluate((el) => { + if (el.childNodes.length > 1) { + const parts: string[] = []; + el.childNodes.forEach((n) => parts.push(n.textContent.trim())); + return parts.join(' '); + } else { + return el.textContent.trim(); + } + }), + await el.evaluate((el) => el.getAttribute('datetime')) + ); + if (date.attribute) { dates.push( - await el.evaluate((el) => { - if (el.childNodes.length > 1) { - const parts: string[] = []; - el.childNodes.forEach((n) => parts.push(n.textContent.trim())); - return parts.join(' '); - } else { - return el.textContent.trim(); - } - }), - await el.evaluate((el) => el.getAttribute('datetime')) + await el.evaluate((el, attr) => el.getAttribute(attr), date.attribute) ); - if (date.attribute) { - dates.push( - await el.evaluate((el, attr) => el.getAttribute(attr), date.attribute) - ); - } - }, - firstMatchOnly: !date.firstOnly, - selector, - }); - } - } - - if (actions.length > 0) { - await this.open(url, actions, { - timeout: typeof publisher.fetchPolicy?.timeout === 'string' ? ms(publisher.fetchPolicy.timeout) : publisher.fetchPolicy?.timeout, - waitUntil: publisher.fetchPolicy?.waitUntil, + } + }, + firstMatchOnly: !date.firstOnly, + selector, }); } + await this.open(url, actions, { + timeout: typeof publisher.fetchPolicy?.timeout === 'string' ? ms(publisher.fetchPolicy.timeout) : publisher.fetchPolicy?.timeout, + waitUntil: publisher.fetchPolicy?.waitUntil, + }); + loot.dateMatches = dates.filter(Boolean); - if (!loot.date) { - loot.date = maxDate(...dates); - if (!loot.date || Number.isNaN(loot.date.valueOf())) { - loot.date = parseDate(dates.join(' ')); - } + loot.date = maxDate(...dates); + if (!loot.date || Number.isNaN(loot.date.valueOf())) { + loot.date = parseDate(dates.join(' ')); } loot.authors = [...new Set(authors.map((a) => clean(a, /^\s*by:?\s*/i).split(/\s*(?:,|and)\s*/).flat()).flat().filter(Boolean))]; - if (!loot.imageUrls || loot.imageUrls.length === 0) { - loot.imageUrls = Array.from(new Set(imageUrls.filter((url) => url && !/\.(gif|svg)/i.test(url)))).slice(0, MAX_IMAGE_COUNT); - } - + loot.imageUrls = Array.from(new Set(imageUrls.filter((url) => url && !/\.(gif|svg)/i.test(url)))).slice(0, MAX_IMAGE_COUNT); + return loot; } diff --git a/src/web/public/images/ss/ss-custom-feed.png b/src/web/public/images/ss/ss-custom-feed.png index 06ccf87c8..acf99e84c 100644 Binary files a/src/web/public/images/ss/ss-custom-feed.png and b/src/web/public/images/ss/ss-custom-feed.png differ diff --git a/src/web/public/images/ss/ss-localization.png b/src/web/public/images/ss/ss-localization.png index 41fc24362..d7bb0212a 100644 Binary files a/src/web/public/images/ss/ss-localization.png and b/src/web/public/images/ss/ss-localization.png differ diff --git a/src/web/public/images/ss/ss-main.png b/src/web/public/images/ss/ss-main.png index cef058e26..9cc5be832 100644 Binary files a/src/web/public/images/ss/ss-main.png and b/src/web/public/images/ss/ss-main.png differ diff --git a/src/web/public/images/ss/ss-publishers.png b/src/web/public/images/ss/ss-publishers.png index 26cdc3917..4331d1f6b 100644 Binary files a/src/web/public/images/ss/ss-publishers.png and b/src/web/public/images/ss/ss-publishers.png differ diff --git a/src/web/public/images/ss/ss-sentiment.png b/src/web/public/images/ss/ss-sentiment.png index 008376837..9162d8799 100644 Binary files a/src/web/public/images/ss/ss-sentiment.png and b/src/web/public/images/ss/ss-sentiment.png differ diff --git a/src/web/src/components/Screenshot.tsx b/src/web/src/components/Screenshot.tsx index 6a49eee87..6ec6a91e6 100644 --- a/src/web/src/components/Screenshot.tsx +++ b/src/web/src/components/Screenshot.tsx @@ -50,7 +50,7 @@ function SCREENS(formFactor = '') { bullets: { image: imageName('ss-bullets') }, customFeed: { image: imageName('ss-custom-feed') }, localization: { image: imageName('ss-localization') }, - main: { image: imageName('ss-main') }, + main: { image: imageName('ss-main'), invert: true }, preview: { image: imageName('ss-quick-preview') }, publishers: { image: imageName('ss-publishers') }, reader: { image: imageName('ss-reader') }, @@ -331,7 +331,7 @@ type ScreenshotCarouselProps = Partial & { }; export const SLIDE_ORDER = [ - 'main', + 'bullets', 'customFeed', 'publishers', 'reader',