From 189e10f2b4b5b3ef69da8a66a41dbaf080978119 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Mon, 6 May 2024 08:37:58 +0200 Subject: [PATCH] Widget: Add mentions widget --- IceCubesApp.xcodeproj/project.pbxproj | 22 +++- ...CubesAppWidgetsExtensionExtension.xcscheme | 114 ++++++++++++++++++ .../HashtagPostsWidget.swift | 14 +-- .../IceCubesAppWidgetsExtensionBundle.swift | 1 + .../LatestPostsWidget.swift | 15 +-- .../LatestPostsWidgetConfiguration.swift | 0 .../MentionWidget/MentionWidget.swift | 81 +++++++++++++ .../MentionWidgetConfiguration.swift | 18 +++ .../Shared/PostsWidgetView.swift | 4 +- 9 files changed, 250 insertions(+), 19 deletions(-) create mode 100644 IceCubesApp.xcodeproj/xcshareddata/xcschemes/IceCubesAppWidgetsExtensionExtension.xcscheme rename IceCubesAppWidgetsExtension/{LatestPosts => LatestPostsWidget}/LatestPostsWidget.swift (86%) rename IceCubesAppWidgetsExtension/{LatestPosts => LatestPostsWidget}/LatestPostsWidgetConfiguration.swift (100%) create mode 100644 IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift create mode 100644 IceCubesAppWidgetsExtension/MentionWidget/MentionWidgetConfiguration.swift diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index a5cc6b79..c2ec3166 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -124,6 +124,8 @@ 9FF2FB632BE7F5D9001560CE /* HashtagPostsWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB5E2BE7F56F001560CE /* HashtagPostsWidgetConfiguration.swift */; }; 9FF2FB672BE7F816001560CE /* PostsWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB652BE7F805001560CE /* PostsWidgetView.swift */; }; 9FF2FB6A2BE7F84E001560CE /* SharedUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB682BE7F842001560CE /* SharedUtils.swift */; }; + 9FF2FB702BE8AE9D001560CE /* MentionWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB6E2BE8AE9B001560CE /* MentionWidgetConfiguration.swift */; }; + 9FF2FB712BE8AEA0001560CE /* MentionWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB6C2BE8AE90001560CE /* MentionWidget.swift */; }; 9FFF677C299B7B2C00FE700A /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677B299B7B2C00FE700A /* Notifications */; }; 9FFF6780299B7D2B00FE700A /* DesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677F299B7D2B00FE700A /* DesignSystem */; }; 9FFF6782299B7D3A00FE700A /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF6781299B7D3A00FE700A /* Account */; }; @@ -298,6 +300,8 @@ 9FF2FB602BE7F5A7001560CE /* HashtagPostsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagPostsWidget.swift; sourceTree = ""; }; 9FF2FB652BE7F805001560CE /* PostsWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostsWidgetView.swift; sourceTree = ""; }; 9FF2FB682BE7F842001560CE /* SharedUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedUtils.swift; sourceTree = ""; }; + 9FF2FB6C2BE8AE90001560CE /* MentionWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionWidget.swift; sourceTree = ""; }; + 9FF2FB6E2BE8AE9B001560CE /* MentionWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionWidgetConfiguration.swift; sourceTree = ""; }; B0BAB49E29B3D7A9008F54D7 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; C4CBB90B298A0DA3007E1707 /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/InfoPlist.strings"; sourceTree = ""; }; C4FBCF6F298FD88A0015DF22 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; @@ -479,9 +483,10 @@ 9F7788CA2BE652B1004E6BEF /* IceCubesAppWidgetsExtension */ = { isa = PBXGroup; children = ( + 9FF2FB6B2BE8AE78001560CE /* MentionWidget */, 9FF2FB642BE7F7FA001560CE /* Shared */, 9FF2FB5D2BE7F559001560CE /* HashtagPostsWidget */, - 9FF2FB5C2BE7F549001560CE /* LatestPosts */, + 9FF2FB5C2BE7F549001560CE /* LatestPostsWidget */, 9F7788D72BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.entitlements */, 9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */, 9F7788D12BE652B2004E6BEF /* Assets.xcassets */, @@ -641,13 +646,13 @@ path = Settings; sourceTree = ""; }; - 9FF2FB5C2BE7F549001560CE /* LatestPosts */ = { + 9FF2FB5C2BE7F549001560CE /* LatestPostsWidget */ = { isa = PBXGroup; children = ( 9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */, 9F7788CF2BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift */, ); - path = LatestPosts; + path = LatestPostsWidget; sourceTree = ""; }; 9FF2FB5D2BE7F559001560CE /* HashtagPostsWidget */ = { @@ -668,6 +673,15 @@ path = Shared; sourceTree = ""; }; + 9FF2FB6B2BE8AE78001560CE /* MentionWidget */ = { + isa = PBXGroup; + children = ( + 9FF2FB6C2BE8AE90001560CE /* MentionWidget.swift */, + 9FF2FB6E2BE8AE9B001560CE /* MentionWidgetConfiguration.swift */, + ); + path = MentionWidget; + sourceTree = ""; + }; E9B576C029743F2A00BCE646 /* Localization */ = { isa = PBXGroup; children = ( @@ -982,11 +996,13 @@ buildActionMask = 2147483647; files = ( 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 */, 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 */, diff --git a/IceCubesApp.xcodeproj/xcshareddata/xcschemes/IceCubesAppWidgetsExtensionExtension.xcscheme b/IceCubesApp.xcodeproj/xcshareddata/xcschemes/IceCubesAppWidgetsExtensionExtension.xcscheme new file mode 100644 index 00000000..31b0b112 --- /dev/null +++ b/IceCubesApp.xcodeproj/xcshareddata/xcschemes/IceCubesAppWidgetsExtensionExtension.xcscheme @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift index 7144d5be..08478b6c 100644 --- a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift @@ -8,7 +8,7 @@ import Timeline struct HashtagPostsWidgetProvider: AppIntentTimelineProvider { func placeholder(in context: Context) -> PostsWidgetEntry { .init(date: Date(), - timeline: .hashtag(tag: "Mastodon", accountId: nil), + title: "#Mastodon", statuses: [.placeholder()], images: [:]) } @@ -18,7 +18,7 @@ struct HashtagPostsWidgetProvider: AppIntentTimelineProvider { return entry } return .init(date: Date(), - timeline: .hashtag(tag: "Mastodon", accountId: nil), + title: "#Mastodon", statuses: [], images: [:]) } @@ -29,18 +29,18 @@ struct HashtagPostsWidgetProvider: AppIntentTimelineProvider { private func timeline(for configuration: HashtagPostsWidgetConfiguration, context: Context) async -> Timeline { do { - let statuses = await loadStatuses(for: .hashtag(tag: configuration.hashgtag, accountId: nil), + let timeline: TimelineFilter = .hashtag(tag: configuration.hashgtag, accountId: nil) + 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(), - timeline: .hashtag(tag: configuration.hashgtag, - accountId: nil), + title: timeline.title, statuses: statuses, images: images)], policy: .atEnd) } catch { return Timeline(entries: [.init(date: Date(), - timeline: .hashtag(tag: "Mastodon", accountId: nil), + title: "#Mastodon", statuses: [], images: [:])], policy: .atEnd) @@ -69,7 +69,7 @@ struct HashtagPostsWidget: Widget { HashtagPostsWidget() } timeline: { PostsWidgetEntry(date: .now, - timeline: .hashtag(tag: "Matodon", accountId: nil), + title: "#Mastodon", statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], images: [:]) } diff --git a/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift b/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift index b950fdb1..ba5f7cea 100644 --- a/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift +++ b/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift @@ -6,5 +6,6 @@ struct IceCubesAppWidgetsExtensionBundle: WidgetBundle { var body: some Widget { LatestPostsWidget() HashtagPostsWidget() + MentionsWidget() } } diff --git a/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidget.swift b/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift similarity index 86% rename from IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidget.swift rename to IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift index 20181a7f..a6484b1c 100644 --- a/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift @@ -8,7 +8,7 @@ import Timeline struct LatestPostsWidgetProvider: AppIntentTimelineProvider { func placeholder(in context: Context) -> PostsWidgetEntry { .init(date: Date(), - timeline: .home, + title: "Home", statuses: [.placeholder()], images: [:]) } @@ -18,7 +18,8 @@ struct LatestPostsWidgetProvider: AppIntentTimelineProvider { return entry } return .init(date: Date(), - timeline: .home, statuses: [], + title: configuration.timeline.timeline.title, + statuses: [], images: [:]) } @@ -33,12 +34,12 @@ struct LatestPostsWidgetProvider: AppIntentTimelineProvider { widgetFamily: context.family) let images = try await loadImages(urls: statuses.map{ $0.account.avatar } ) return Timeline(entries: [.init(date: Date(), - timeline: configuration.timeline.timeline, + title: configuration.timeline.timeline.title, statuses: statuses, images: images)], policy: .atEnd) } catch { return Timeline(entries: [.init(date: Date(), - timeline: .home, + title: configuration.timeline.timeline.title, statuses: [], images: [:])], policy: .atEnd) @@ -86,7 +87,7 @@ struct LatestPostsWidget: Widget { LatestPostsWidget() } timeline: { PostsWidgetEntry(date: .now, - timeline: .home, - statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], - images: [:]) + title: "Mastodon", + statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], + images: [:]) } diff --git a/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidgetConfiguration.swift similarity index 100% rename from IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidgetConfiguration.swift rename to IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidgetConfiguration.swift diff --git a/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift new file mode 100644 index 00000000..046dc74c --- /dev/null +++ b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift @@ -0,0 +1,81 @@ +import WidgetKit +import SwiftUI +import Network +import DesignSystem +import Models +import Timeline + +struct MentionsWidgetProvider: AppIntentTimelineProvider { + func placeholder(in context: Context) -> PostsWidgetEntry { + .init(date: Date(), + title: "Mentions", + statuses: [.placeholder()], + images: [:]) + } + + func snapshot(for configuration: MentionsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry { + if let entry = await timeline(for: configuration, context: context).entries.first { + return entry + } + return .init(date: Date(), + title: "Mentions", + statuses: [], + images: [:]) + } + + func timeline(for configuration: MentionsWidgetConfiguration, in context: Context) async -> Timeline { + await timeline(for: configuration, context: context) + } + + private func timeline(for configuration: MentionsWidgetConfiguration, context: Context) async -> Timeline { + do { + let client = Client(server: configuration.account.account.server, + oauthToken: configuration.account.account.oauthToken) + var excludedTypes = Models.Notification.NotificationType.allCases + excludedTypes.removeAll(where: { $0 == .mention }) + var notifications: [Models.Notification] = + try await client.get(endpoint: Notifications.notifications(minId: nil, + maxId: nil, + types: excludedTypes.map(\.rawValue), + limit: 5)) + let statuses = notifications.compactMap{ $0.status } + let images = try await loadImages(urls: statuses.map{ $0.account.avatar } ) + return Timeline(entries: [.init(date: Date(), + title: "Mentions", + statuses: statuses, + images: images)], policy: .atEnd) + } catch { + return Timeline(entries: [.init(date: Date(), + title: "Mentions", + statuses: [], + images: [:])], + policy: .atEnd) + } + } +} + +struct MentionsWidget: Widget { + let kind: String = "MentionsWidget" + + var body: some WidgetConfiguration { + AppIntentConfiguration(kind: kind, + intent: MentionsWidgetConfiguration.self, + provider: MentionsWidgetProvider()) { entry in + PostsWidgetView(entry: entry) + .containerBackground(Color("WidgetBackground").gradient, for: .widget) + } + .configurationDisplayName("Mentions") + .description("Show the latest mentions for the selected account.") + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge]) + } +} + + +#Preview(as: .systemMedium) { + MentionsWidget() +} timeline: { + PostsWidgetEntry(date: .now, + title: "Mentions", + statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], + images: [:]) +} diff --git a/IceCubesAppWidgetsExtension/MentionWidget/MentionWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidgetConfiguration.swift new file mode 100644 index 00000000..7ad55a49 --- /dev/null +++ b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidgetConfiguration.swift @@ -0,0 +1,18 @@ +import WidgetKit +import AppIntents + +struct MentionsWidgetConfiguration: WidgetConfigurationIntent { + static let title: LocalizedStringResource = "Configuration" + static let description = IntentDescription("Choose the account for this widget") + + @Parameter(title: "Account") + var account: AppAccountEntity +} + +extension MentionsWidgetConfiguration { + static var previewAccount: MentionsWidgetConfiguration { + let intent = MentionsWidgetConfiguration() + intent.account = .init(account: .init(server: "Test", accountName: "Test account")) + return intent + } +} diff --git a/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift b/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift index dcda3780..7b6c6821 100644 --- a/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift +++ b/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift @@ -7,7 +7,7 @@ import Timeline struct PostsWidgetEntry: TimelineEntry { let date: Date - let timeline: TimelineFilter + let title: String let statuses: [Status] let images: [URL: UIImage] } @@ -39,7 +39,7 @@ struct PostsWidgetView : View { private var headerView: some View { HStack { - Text(entry.timeline.title) + Text(entry.title) Spacer() Image(systemName: "cube") }