diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index d4a2473a..3876f12d 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -57,6 +57,9 @@ 9F4A48192976B21900A1A038 /* ProfileTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4A48182976B21900A1A038 /* ProfileTab.swift */; }; 9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F55C68C2955968700F94077 /* ExploreTab.swift */; }; 9F55C6902955993C00F94077 /* Explore in Frameworks */ = {isa = PBXBuildFile; productRef = 9F55C68F2955993C00F94077 /* Explore */; }; + 9F5BE6272BF492CF0074387E /* ListEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5BE6252BF48FE10074387E /* ListEntity.swift */; }; + 9F5BE6282BF492D10074387E /* ListsWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5BE6232BF48FC40074387E /* ListsWidgetConfiguration.swift */; }; + 9F5BE6292BF492D40074387E /* ListsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5BE6212BF48FBA0074387E /* ListsWidget.swift */; }; 9F5E581929545BE700A53960 /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9F5E581829545BE700A53960 /* Env */; }; 9F6028562B3F36AE00476078 /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6028552B3F36AE00476078 /* AppView.swift */; }; 9F6028582B3F3B7600476078 /* ToolbarTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6028572B3F3B7600476078 /* ToolbarTab.swift */; }; @@ -245,6 +248,9 @@ 9F4A48182976B21900A1A038 /* ProfileTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTab.swift; sourceTree = ""; }; 9F55C68C2955968700F94077 /* ExploreTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreTab.swift; sourceTree = ""; }; 9F55C68E295598F900F94077 /* Explore */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Explore; path = Packages/Explore; sourceTree = ""; }; + 9F5BE6212BF48FBA0074387E /* ListsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsWidget.swift; sourceTree = ""; }; + 9F5BE6232BF48FC40074387E /* ListsWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsWidgetConfiguration.swift; sourceTree = ""; }; + 9F5BE6252BF48FE10074387E /* ListEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListEntity.swift; sourceTree = ""; }; 9F5E581729545B5500A53960 /* Env */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Env; path = Packages/Env; sourceTree = ""; }; 9F6028552B3F36AE00476078 /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = ""; }; 9F6028572B3F3B7600476078 /* ToolbarTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarTab.swift; sourceTree = ""; }; @@ -433,6 +439,7 @@ 9F37BDDA2BE36E22007F28AD /* PostIntent.swift */, 9F37BDDE2BE37C35007F28AD /* TabIntent.swift */, 9F7788EC2BE78D75004E6BEF /* TimelineFilterEntity.swift */, + 9F5BE6252BF48FE10074387E /* ListEntity.swift */, ); path = IceCubesAppIntents; sourceTree = ""; @@ -461,6 +468,15 @@ path = Resources; sourceTree = ""; }; + 9F5BE6202BF48FB20074387E /* ListsWidget */ = { + isa = PBXGroup; + children = ( + 9F5BE6212BF48FBA0074387E /* ListsWidget.swift */, + 9F5BE6232BF48FC40074387E /* ListsWidgetConfiguration.swift */, + ); + path = ListsWidget; + sourceTree = ""; + }; 9F654BF0299AC46200D27FA5 /* Report */ = { isa = PBXGroup; children = ( @@ -481,6 +497,7 @@ 9F7788CA2BE652B1004E6BEF /* IceCubesAppWidgetsExtension */ = { isa = PBXGroup; children = ( + 9F5BE6202BF48FB20074387E /* ListsWidget */, 9FF2FB6B2BE8AE78001560CE /* MentionWidget */, 9FF2FB642BE7F7FA001560CE /* Shared */, 9FF2FB5D2BE7F559001560CE /* HashtagPostsWidget */, @@ -991,17 +1008,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9F5BE6272BF492CF0074387E /* ListEntity.swift in Sources */, 9FF2FB622BE7F5D5001560CE /* HashtagPostsWidget.swift in Sources */, 9FF2FB712BE8AEA0001560CE /* MentionWidget.swift in Sources */, 9F7788EA2BE65585004E6BEF /* AppAccountEntity.swift in Sources */, 9FF2FB6A2BE7F84E001560CE /* SharedUtils.swift in Sources */, 9F7788CE2BE652B1004E6BEF /* LatestPostsWidget.swift in Sources */, 9F7788EE2BE78D7B004E6BEF /* TimelineFilterEntity.swift in Sources */, + 9F5BE6292BF492D40074387E /* ListsWidget.swift in Sources */, 9F7788CC2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift in Sources */, 9FF2FB702BE8AE9D001560CE /* MentionWidgetConfiguration.swift in Sources */, 9F7788D02BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift in Sources */, 9FF2FB672BE7F816001560CE /* PostsWidgetView.swift in Sources */, 9FF2FB632BE7F5D9001560CE /* HashtagPostsWidgetConfiguration.swift in Sources */, + 9F5BE6282BF492D10074387E /* ListsWidgetConfiguration.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/IceCubesAppIntents/ListEntity.swift b/IceCubesAppIntents/ListEntity.swift new file mode 100644 index 00000000..cb752998 --- /dev/null +++ b/IceCubesAppIntents/ListEntity.swift @@ -0,0 +1,55 @@ +import AppAccount +import AppIntents +import Env +import Foundation +import Models +import Network +import Timeline + +public struct ListEntity: Identifiable, AppEntity { + public var id: String { list.id } + + public let list: Models.List + + public static let defaultQuery = DefaultListEntityQuery() + + public static let typeDisplayRepresentation: TypeDisplayRepresentation = "List" + + public var displayRepresentation: DisplayRepresentation { + DisplayRepresentation(title: "\(list.title)") + } +} + +public struct DefaultListEntityQuery: EntityQuery { + public init() {} + + @IntentParameterDependency( + \.$account + ) + var account + + public func entities(for _: [ListEntity.ID]) async throws -> [ListEntity] { + await fetchLists().map{ .init(list: $0 )} + } + + public func suggestedEntities() async throws -> [ListEntity] { + await fetchLists().map{ .init(list: $0 )} + } + + public func defaultResult() async -> ListEntity? { + nil + } + + private func fetchLists() async -> [Models.List] { + guard let account = account?.account.account else { + return [] + } + let client = Client(server: account.server, oauthToken: account.oauthToken) + do { + let lists: [Models.List] = try await client.get(endpoint: Lists.lists) + return lists + } catch { + return [] + } + } +} diff --git a/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift b/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift index f4684eb0..f51a57cf 100644 --- a/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift +++ b/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift @@ -6,6 +6,7 @@ struct IceCubesAppWidgetsExtensionBundle: WidgetBundle { var body: some Widget { LatestPostsWidget() HashtagPostsWidget() + ListsPostWidget() MentionsWidget() } } diff --git a/IceCubesAppWidgetsExtension/ListsWidget/ListsWidget.swift b/IceCubesAppWidgetsExtension/ListsWidget/ListsWidget.swift new file mode 100644 index 00000000..1cc488b2 --- /dev/null +++ b/IceCubesAppWidgetsExtension/ListsWidget/ListsWidget.swift @@ -0,0 +1,75 @@ +import DesignSystem +import Models +import Network +import SwiftUI +import Timeline +import WidgetKit + +struct ListsWidgetProvider: AppIntentTimelineProvider { + func placeholder(in _: Context) -> PostsWidgetEntry { + .init(date: Date(), + title: "List name", + statuses: [.placeholder()], + images: [:]) + } + + func snapshot(for configuration: ListsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry { + if let entry = await timeline(for: configuration, context: context).entries.first { + return entry + } + return .init(date: Date(), + title: "List name", + statuses: [], + images: [:]) + } + + func timeline(for configuration: ListsWidgetConfiguration, in context: Context) async -> Timeline { + await timeline(for: configuration, context: context) + } + + private func timeline(for configuration: ListsWidgetConfiguration, context: Context) async -> Timeline { + do { + let timeline: TimelineFilter = .list(list: configuration.timeline.list) + let statuses = await loadStatuses(for: timeline, + account: configuration.account, + widgetFamily: context.family) + let images = try await loadImages(urls: statuses.map { $0.account.avatar }) + return Timeline(entries: [.init(date: Date(), + title: timeline.title, + statuses: statuses, + images: images)], policy: .atEnd) + } catch { + return Timeline(entries: [.init(date: Date(), + title: "List name", + statuses: [], + images: [:])], + policy: .atEnd) + } + } +} + +struct ListsPostWidget: Widget { + let kind: String = "ListsPostWidget" + + var body: some WidgetConfiguration { + AppIntentConfiguration(kind: kind, + intent: ListsWidgetConfiguration.self, + provider: ListsWidgetProvider()) + { entry in + PostsWidgetView(entry: entry) + .containerBackground(Color("WidgetBackground").gradient, for: .widget) + } + .configurationDisplayName("List timeline") + .description("Show the latest post for the selected list") + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge]) + } +} + +#Preview(as: .systemMedium) { + ListsPostWidget() +} timeline: { + PostsWidgetEntry(date: .now, + title: "List name", + statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], + images: [:]) +} diff --git a/IceCubesAppWidgetsExtension/ListsWidget/ListsWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/ListsWidget/ListsWidgetConfiguration.swift new file mode 100644 index 00000000..62015f6c --- /dev/null +++ b/IceCubesAppWidgetsExtension/ListsWidget/ListsWidgetConfiguration.swift @@ -0,0 +1,21 @@ +import AppIntents +import WidgetKit + +struct ListsWidgetConfiguration: WidgetConfigurationIntent { + static let title: LocalizedStringResource = "Configuration" + static let description = IntentDescription("Choose the account and list for this widget") + + @Parameter(title: "Account") + var account: AppAccountEntity + + @Parameter(title: "List") + var timeline: ListEntity +} + +extension ListsWidgetConfiguration { + static var previewAccount: LatestPostsWidgetConfiguration { + let intent = LatestPostsWidgetConfiguration() + intent.account = .init(account: .init(server: "Test", accountName: "Test account")) + return intent + } +}