diff --git a/Bubble/Data/Metrics/MetricsManager.swift b/Bubble/Data/Metrics/MetricsManager.swift new file mode 100644 index 0000000..09d31e2 --- /dev/null +++ b/Bubble/Data/Metrics/MetricsManager.swift @@ -0,0 +1,144 @@ +// Made by Lumaa + +import Foundation +import Charts + +/// This gets metrics on the selected account from the ``Client`` +final class MetricsManager { + private var client: Client + + private var measured: Bool = false + + private(set) public var postCount: [IntData] = [] + private(set) public var postType: [StatusTypeData] = [] + + init(accountManager: AccountManager) { + if let cli = accountManager.getClient() { + self.client = cli + } + fatalError("Account Manager doesn't have a Client bound") + } + + init(client: Client) { + self.client = client + } + + /// Only gets the last few posts count AND ``StatusTypeData`` + private func getLastPosts() async -> ([IntData], [StatusTypeData]) { + guard let accountDetail: Account = try? await self.client.get(endpoint: Accounts.verifyCredentials), let statusesCount: Int = accountDetail.statusesCount else { + fatalError("Couldn't verify creds for Metrics") + } + + if let posts: [Status] = try? await self.client.get( + endpoint: Accounts.statuses(id: accountDetail.id, sinceId: nil, tag: nil, onlyMedia: nil, excludeReplies: nil, pinned: nil) + ) { + var countData: [IntData] = [] + var typeData: [StatusTypeData] = [] + + posts.reversed().forEach { post in // go through posts backwards + let i: Int = posts.firstIndex(of: post) ?? -1 // latest post is first, oldest is last + + let newCountData: IntData = .init(date: post.createdAt.asDate, count: statusesCount - i, fullCount: statusesCount) + let newTypeData: StatusTypeData = .init(date: post.createdAt.asDate, type: post.getType()) + + countData.append(newCountData) + typeData.append(newTypeData) + } + + return (countData, typeData) + } else { + fatalError("Couldn't fetch account's statuses") + } + } + + /// Data used for integer Metrics + struct IntData: GraphData { + let date: Date + let count: Int + let fullCount: Int + + var difference: Int { + fullCount - count + } + + var plottableCount: PlottableValue { + .value(String(localized: "metrics.status.count"), count) + } + + init(date: Date, count: Int, fullCount: Int) { + self.date = date + self.count = count + self.fullCount = fullCount + } + } + + struct StatusTypeData: GraphData { + let date: Date + let type: Status.StatusType + + var label: String { + self.type.localized + } + + var plottableType: PlottableValue { + .value(String(localized: "metrics.status.count"), label) + } + + init(date: Date, type: Status.StatusType) { + self.date = date + self.type = type + } + } + + protocol GraphData { + var date: Date { get } + } +} + +extension MetricsManager.GraphData { + var plottableDate: PlottableValue { + .value(String(localized: "metrics.any.date"), date) + } +} + +// MARK: - Status Type +extension Status { + func getType() -> Status.StatusType { + let isReply: Bool = self.inReplyToId != nil + let isDirect: Bool = self.visibility == .direct + + if !isReply && !isDirect { + return Self.StatusType.post + } else if isReply && !isDirect { + return Self.StatusType.reply + } else if !isReply && isDirect { + return Self.StatusType.direct + } else if isReply && isDirect { + return Self.StatusType.directReply + } + return Self.StatusType.post + } + + enum StatusType { + case post + case reply + case direct + case directReply + case unknown + + var localized: String { + switch self { + case .post: + String(localized: "status.type.post") + case .reply: + String(localized: "status.type.reply") + case .direct: + String(localized: "status.type.direct") + case .directReply: + String(localized: "status.type.direct-reply") + default: + String(localized: "status.type.unknown") + } + } + } +} diff --git a/Bubble/Localizable.xcstrings b/Bubble/Localizable.xcstrings index f120e0e..ad065d0 100644 --- a/Bubble/Localizable.xcstrings +++ b/Bubble/Localizable.xcstrings @@ -2072,6 +2072,38 @@ } } }, + "metrics.any.date" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Date" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Date" + } + } + } + }, + "metrics.status.count" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Post Count" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre de publications" + } + } + } + }, "posting.alt.apply" : { "localizations" : { "en" : { @@ -4370,6 +4402,86 @@ } } }, + "status.type.direct" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Direct Message" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Message privé" + } + } + } + }, + "status.type.direct-reply" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reply to a Message" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Réponse à un message" + } + } + } + }, + "status.type.post" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Post" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Publication" + } + } + } + }, + "status.type.reply" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reply" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Réponse" + } + } + } + }, + "status.type.unknown" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Undetermined" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Indéterminé" + } + } + } + }, "support" : { "localizations" : { "en" : {