2023-02-06 11:39:40 +01:00
|
|
|
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
|
|
|
|
|
|
|
import WidgetKit
|
|
|
|
import SwiftUI
|
|
|
|
import Intents
|
|
|
|
import MastodonSDK
|
2023-02-07 09:33:47 +01:00
|
|
|
import MastodonLocalization
|
2023-12-31 16:44:54 +01:00
|
|
|
import MastodonCore
|
2023-02-06 11:39:40 +01:00
|
|
|
|
|
|
|
struct LatestFollowersWidgetProvider: IntentTimelineProvider {
|
|
|
|
func placeholder(in context: Context) -> LatestFollowersEntry {
|
|
|
|
.placeholder
|
|
|
|
}
|
|
|
|
|
|
|
|
func getSnapshot(for configuration: LatestFollowersIntent, in context: Context, completion: @escaping (LatestFollowersEntry) -> ()) {
|
|
|
|
loadCurrentEntry(for: configuration, in: context, completion: completion)
|
|
|
|
}
|
|
|
|
|
|
|
|
func getTimeline(for configuration: LatestFollowersIntent, in context: Context, completion: @escaping (Timeline<LatestFollowersEntry>) -> ()) {
|
|
|
|
loadCurrentEntry(for: configuration, in: context) { entry in
|
|
|
|
completion(Timeline(entries: [entry], policy: .after(.now)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct LatestFollowersEntry: TimelineEntry {
|
|
|
|
let date: Date
|
|
|
|
let accounts: [LatestFollowersEntryAccountable]?
|
|
|
|
let configuration: LatestFollowersIntent
|
|
|
|
|
|
|
|
static var placeholder: Self {
|
|
|
|
LatestFollowersEntry(
|
|
|
|
date: .now,
|
|
|
|
accounts: [
|
|
|
|
LatestFollowersEntryAccount(
|
|
|
|
note: "Just another Mastodon user",
|
|
|
|
displayNameWithFallback: "Mastodon",
|
|
|
|
acct: "mastodon",
|
|
|
|
avatarImage: UIImage(named: "missingAvatar")!,
|
|
|
|
domain: "mastodon"
|
|
|
|
),
|
|
|
|
LatestFollowersEntryAccount(
|
|
|
|
note: "Yet another Mastodon user",
|
|
|
|
displayNameWithFallback: "Mastodon",
|
|
|
|
acct: "mastodon",
|
|
|
|
avatarImage: UIImage(named: "missingAvatar")!,
|
|
|
|
domain: "mastodon"
|
|
|
|
)
|
|
|
|
],
|
|
|
|
configuration: LatestFollowersIntent()
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
static var unconfigured: Self {
|
|
|
|
LatestFollowersEntry(
|
|
|
|
date: .now,
|
2023-02-06 13:51:58 +01:00
|
|
|
accounts: nil,
|
2023-02-06 11:39:40 +01:00
|
|
|
configuration: LatestFollowersIntent()
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct LatestFollowersWidget: Widget {
|
|
|
|
private var availableFamilies: [WidgetFamily] {
|
|
|
|
return [.systemSmall, .systemMedium]
|
|
|
|
}
|
|
|
|
|
|
|
|
var body: some WidgetConfiguration {
|
|
|
|
IntentConfiguration(kind: "Latest followers", intent: LatestFollowersIntent.self, provider: LatestFollowersWidgetProvider()) { entry in
|
|
|
|
LatestFollowersWidgetView(entry: entry)
|
|
|
|
}
|
2023-02-07 09:33:47 +01:00
|
|
|
.configurationDisplayName(L10n.Widget.LatestFollowers.configurationDisplayName)
|
|
|
|
.description(L10n.Widget.LatestFollowers.configurationDescription)
|
2023-02-06 11:39:40 +01:00
|
|
|
.supportedFamilies(availableFamilies)
|
2024-02-20 16:34:40 +01:00
|
|
|
.contentMarginsDisabled() // Disable excessive margins (only effective for iOS >= 17.0
|
2023-02-06 11:39:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private extension LatestFollowersWidgetProvider {
|
|
|
|
func loadCurrentEntry(for configuration: LatestFollowersIntent, in context: Context, completion: @escaping (LatestFollowersEntry) -> Void) {
|
|
|
|
Task { @MainActor in
|
2023-12-31 16:44:54 +01:00
|
|
|
|
|
|
|
AuthenticationServiceProvider.shared.restore()
|
|
|
|
|
2023-02-06 11:39:40 +01:00
|
|
|
guard
|
|
|
|
let authBox = WidgetExtension.appContext
|
|
|
|
.authenticationService
|
|
|
|
.mastodonAuthenticationBoxes
|
|
|
|
.first
|
|
|
|
else {
|
2023-02-06 11:46:46 +01:00
|
|
|
guard !context.isPreview else {
|
|
|
|
return completion(.placeholder)
|
|
|
|
}
|
2023-02-06 11:39:40 +01:00
|
|
|
return completion(.unconfigured)
|
|
|
|
}
|
2023-02-06 11:46:46 +01:00
|
|
|
|
2023-02-06 11:39:40 +01:00
|
|
|
var accounts = [LatestFollowersEntryAccountable]()
|
|
|
|
|
|
|
|
let followers = try await WidgetExtension.appContext
|
|
|
|
.apiService
|
|
|
|
.followers(userID: authBox.userID, maxID: nil, authenticationBox: authBox)
|
|
|
|
.value
|
|
|
|
.prefix(2) // X most recent followers
|
|
|
|
|
|
|
|
for follower in followers {
|
|
|
|
let imageData = try await URLSession.shared.data(from: follower.avatarImageURLWithFallback(domain: authBox.domain)).0
|
|
|
|
|
|
|
|
accounts.append(
|
|
|
|
LatestFollowersEntryAccount(
|
|
|
|
note: follower.note,
|
|
|
|
displayNameWithFallback: follower.displayNameWithFallback,
|
|
|
|
acct: follower.acct,
|
|
|
|
avatarImage: UIImage(data: imageData) ?? UIImage(named: "missingAvatar")!,
|
|
|
|
domain: authBox.domain
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
let entry = LatestFollowersEntry(
|
|
|
|
date: Date(),
|
|
|
|
accounts: accounts,
|
|
|
|
configuration: configuration
|
|
|
|
)
|
|
|
|
|
|
|
|
completion(entry)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protocol LatestFollowersEntryAccountable {
|
|
|
|
var note: String { get }
|
|
|
|
var displayNameWithFallback: String { get }
|
|
|
|
var acct: String { get }
|
|
|
|
var avatarImage: UIImage { get }
|
|
|
|
var domain: String { get }
|
|
|
|
}
|
|
|
|
|
|
|
|
struct LatestFollowersEntryAccount: LatestFollowersEntryAccountable {
|
|
|
|
let note: String
|
|
|
|
let displayNameWithFallback: String
|
|
|
|
let acct: String
|
|
|
|
let avatarImage: UIImage
|
|
|
|
let domain: String
|
|
|
|
|
|
|
|
static func from(mastodonAccount: Mastodon.Entity.Account, domain: String, avatarImage: UIImage) -> Self {
|
|
|
|
LatestFollowersEntryAccount(
|
|
|
|
note: mastodonAccount.header,
|
|
|
|
displayNameWithFallback: mastodonAccount.displayNameWithFallback,
|
|
|
|
acct: mastodonAccount.acct,
|
|
|
|
avatarImage: avatarImage,
|
|
|
|
domain: domain
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|