2023-01-24 11:14:50 +01:00
|
|
|
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
|
|
|
|
|
|
|
import WidgetKit
|
|
|
|
import SwiftUI
|
|
|
|
import Intents
|
2023-01-24 12:47:48 +01:00
|
|
|
import MastodonSDK
|
2023-12-31 16:44:54 +01:00
|
|
|
import MastodonCore
|
2023-02-07 09:33:47 +01:00
|
|
|
import MastodonLocalization
|
2023-01-24 11:14:50 +01:00
|
|
|
|
2023-01-31 11:43:45 +01:00
|
|
|
struct FollowersCountWidgetProvider: IntentTimelineProvider {
|
2023-01-30 15:28:16 +01:00
|
|
|
private let followersHistory = FollowersCountHistory.shared
|
|
|
|
|
2023-01-31 11:43:45 +01:00
|
|
|
func placeholder(in context: Context) -> FollowersCountEntry {
|
2023-01-24 15:23:25 +01:00
|
|
|
.placeholder
|
2023-01-24 11:14:50 +01:00
|
|
|
}
|
|
|
|
|
2023-01-31 11:43:45 +01:00
|
|
|
func getSnapshot(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (FollowersCountEntry) -> ()) {
|
2023-01-24 12:47:48 +01:00
|
|
|
loadCurrentEntry(for: configuration, in: context, completion: completion)
|
2023-01-24 11:14:50 +01:00
|
|
|
}
|
|
|
|
|
2023-01-31 11:43:45 +01:00
|
|
|
func getTimeline(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (Timeline<FollowersCountEntry>) -> ()) {
|
2023-01-24 12:47:48 +01:00
|
|
|
loadCurrentEntry(for: configuration, in: context) { entry in
|
|
|
|
completion(Timeline(entries: [entry], policy: .after(.now)))
|
2023-01-24 11:14:50 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-31 11:43:45 +01:00
|
|
|
struct FollowersCountEntry: TimelineEntry {
|
2023-01-24 11:14:50 +01:00
|
|
|
let date: Date
|
2023-01-24 15:23:25 +01:00
|
|
|
let account: FollowersEntryAccountable?
|
2023-01-24 11:14:50 +01:00
|
|
|
let configuration: FollowersCountIntent
|
2023-01-24 12:47:48 +01:00
|
|
|
|
2023-01-24 15:23:25 +01:00
|
|
|
static var placeholder: Self {
|
2023-01-31 11:43:45 +01:00
|
|
|
FollowersCountEntry(
|
2023-01-24 15:23:25 +01:00
|
|
|
date: .now,
|
|
|
|
account: FollowersEntryAccount(
|
|
|
|
followersCount: 99_900,
|
|
|
|
displayNameWithFallback: "Mastodon",
|
|
|
|
acct: "mastodon",
|
2023-01-30 15:28:16 +01:00
|
|
|
avatarImage: UIImage(named: "missingAvatar")!,
|
|
|
|
domain: "mastodon"
|
2023-01-24 15:23:25 +01:00
|
|
|
),
|
|
|
|
configuration: FollowersCountIntent()
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
static var unconfigured: Self {
|
2023-01-31 11:43:45 +01:00
|
|
|
FollowersCountEntry(
|
2023-01-24 15:23:25 +01:00
|
|
|
date: .now,
|
|
|
|
account: nil,
|
|
|
|
configuration: FollowersCountIntent()
|
|
|
|
)
|
2023-01-24 12:47:48 +01:00
|
|
|
}
|
2023-01-24 11:14:50 +01:00
|
|
|
}
|
|
|
|
|
2023-01-31 11:43:45 +01:00
|
|
|
struct FollowersCountWidget: Widget {
|
2023-01-26 16:22:18 +01:00
|
|
|
private var availableFamilies: [WidgetFamily] {
|
2023-09-29 19:31:22 +02:00
|
|
|
return [.systemSmall, .accessoryRectangular, .accessoryCircular]
|
2023-01-24 11:14:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
var body: some WidgetConfiguration {
|
2023-01-31 11:43:45 +01:00
|
|
|
IntentConfiguration(kind: "Followers", intent: FollowersCountIntent.self, provider: FollowersCountWidgetProvider()) { entry in
|
|
|
|
FollowersCountWidgetView(entry: entry)
|
2023-01-24 11:14:50 +01:00
|
|
|
}
|
2023-02-07 09:33:47 +01:00
|
|
|
.configurationDisplayName(L10n.Widget.FollowersCount.configurationDisplayName)
|
|
|
|
.description(L10n.Widget.FollowersCount.configurationDescription)
|
2023-01-26 16:22:18 +01:00
|
|
|
.supportedFamilies(availableFamilies)
|
2024-02-20 16:34:40 +01:00
|
|
|
.contentMarginsDisabled() // Disable excessive margins (only effective for iOS >= 17.0
|
2023-01-24 12:47:48 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-31 11:43:45 +01:00
|
|
|
private extension FollowersCountWidgetProvider {
|
|
|
|
func loadCurrentEntry(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (FollowersCountEntry) -> Void) {
|
2023-01-24 12:47:48 +01:00
|
|
|
Task {
|
2023-12-31 16:44:54 +01:00
|
|
|
|
|
|
|
AuthenticationServiceProvider.shared.restore()
|
|
|
|
|
2023-01-24 12:47:48 +01:00
|
|
|
guard
|
|
|
|
let authBox = WidgetExtension.appContext
|
|
|
|
.authenticationService
|
|
|
|
.mastodonAuthenticationBoxes
|
2023-01-26 15:15:59 +01:00
|
|
|
.first
|
2023-01-24 12:47:48 +01:00
|
|
|
else {
|
2023-02-06 11:46:46 +01:00
|
|
|
guard !context.isPreview else {
|
|
|
|
return completion(.placeholder)
|
|
|
|
}
|
2023-01-24 15:23:25 +01:00
|
|
|
return completion(.unconfigured)
|
2023-01-24 12:47:48 +01:00
|
|
|
}
|
2023-01-26 15:15:59 +01:00
|
|
|
|
2023-02-07 09:07:37 +01:00
|
|
|
guard
|
2024-01-02 23:09:50 +01:00
|
|
|
let desiredAccount = configuration.account ?? authBox.authentication.account()?.acctWithDomain
|
2023-02-07 09:07:37 +01:00
|
|
|
else {
|
2023-01-26 15:15:59 +01:00
|
|
|
return completion(.unconfigured)
|
|
|
|
}
|
|
|
|
|
2023-02-07 09:07:37 +01:00
|
|
|
guard
|
|
|
|
let resultingAccount = try await WidgetExtension.appContext
|
|
|
|
.apiService
|
|
|
|
.search(query: .init(q: desiredAccount, type: .accounts), authenticationBox: authBox)
|
|
|
|
.value
|
|
|
|
.accounts
|
2023-02-07 12:36:13 +01:00
|
|
|
.first(where: { $0.acctWithDomainIfMissing(authBox.domain) == desiredAccount })
|
2023-02-07 09:07:37 +01:00
|
|
|
else {
|
|
|
|
return completion(.unconfigured)
|
|
|
|
}
|
2023-01-24 12:47:48 +01:00
|
|
|
|
2023-01-24 15:23:25 +01:00
|
|
|
let imageData = try await URLSession.shared.data(from: resultingAccount.avatarImageURLWithFallback(domain: authBox.domain)).0
|
2023-01-24 12:47:48 +01:00
|
|
|
|
2023-01-31 11:43:45 +01:00
|
|
|
let entry = FollowersCountEntry(
|
2023-01-24 12:47:48 +01:00
|
|
|
date: Date(),
|
2023-01-24 15:23:25 +01:00
|
|
|
account: FollowersEntryAccount.from(
|
|
|
|
mastodonAccount: resultingAccount,
|
2023-01-30 15:28:16 +01:00
|
|
|
domain: authBox.domain,
|
2023-01-24 15:23:25 +01:00
|
|
|
avatarImage: UIImage(data: imageData) ?? UIImage(named: "missingAvatar")!
|
|
|
|
),
|
2023-01-24 12:47:48 +01:00
|
|
|
configuration: configuration
|
|
|
|
)
|
2023-01-30 15:28:16 +01:00
|
|
|
|
|
|
|
followersHistory.updateFollowersTodayCount(
|
|
|
|
account: entry.account!,
|
|
|
|
count: resultingAccount.followersCount
|
|
|
|
)
|
|
|
|
|
2023-01-24 12:47:48 +01:00
|
|
|
completion(entry)
|
|
|
|
}
|
2023-01-24 11:14:50 +01:00
|
|
|
}
|
|
|
|
}
|
2023-01-24 15:23:25 +01:00
|
|
|
|
|
|
|
protocol FollowersEntryAccountable {
|
|
|
|
var followersCount: Int { get }
|
|
|
|
var displayNameWithFallback: String { get }
|
|
|
|
var acct: String { get }
|
|
|
|
var avatarImage: UIImage { get }
|
2023-01-30 15:28:16 +01:00
|
|
|
var domain: String { get }
|
2023-01-24 15:23:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
struct FollowersEntryAccount: FollowersEntryAccountable {
|
|
|
|
let followersCount: Int
|
|
|
|
let displayNameWithFallback: String
|
|
|
|
let acct: String
|
|
|
|
let avatarImage: UIImage
|
2023-01-30 15:28:16 +01:00
|
|
|
let domain: String
|
2023-01-24 15:23:25 +01:00
|
|
|
|
2023-01-30 15:28:16 +01:00
|
|
|
static func from(mastodonAccount: Mastodon.Entity.Account, domain: String, avatarImage: UIImage) -> Self {
|
2023-01-24 15:23:25 +01:00
|
|
|
FollowersEntryAccount(
|
|
|
|
followersCount: mastodonAccount.followersCount,
|
|
|
|
displayNameWithFallback: mastodonAccount.displayNameWithFallback,
|
|
|
|
acct: mastodonAccount.acct,
|
2023-01-30 15:28:16 +01:00
|
|
|
avatarImage: avatarImage,
|
|
|
|
domain: domain
|
2023-01-24 15:23:25 +01:00
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|