b8114da874
/interactions/ already exposes the full actor lists for boosts, reactions and replies in a single relay call — no reason for the timeline row to keep working blind. This wires that data into PostRowView and adds a relay-side cross-check so "did I already boost/vote on this?" survives even if the local optimistic cache was cleared. OrgSocialInteractions gains boostURLs: [String]; ThreadClient parses data.boosts. PostRowView swaps the fetchRawThread call for fetchInteractions (same endpoint family, already needed for the reply/reaction counts) and uses the response to: - render a boost count badge next to the Boost icon, - make each reaction chip a Button that presents ReactorsSheet listing the feed hostnames that reacted with that emoji, - show a tiny ·-separated list of voter hostnames under each poll option (derived from OrgSocialPollVote.voterURLs, which already existed), - flip boostViewModel.boosted to true when the relay's booster list contains a URL starting with the viewer's own feed — regardless of local cache state, - flip pollVoteViewModel.votedOption to the matching option when the relay's voters list contains the viewer's own feed prefix — covers votes cast from another client. Local OwnInteractionCache still acts as the fast-path instant read for just-written actions (relay lags ~60s). The relay check is a second source of truth layered on top, so cross-client and cross-device consistency improves without adding extra round-trips.
91 lines
2.9 KiB
Swift
91 lines
2.9 KiB
Swift
import Foundation
|
|
|
|
/// Emoji reactions on a post in a thread.
|
|
public struct OrgSocialMood: Sendable, Equatable {
|
|
public let emoji: String
|
|
public let posts: [String]
|
|
|
|
public init(emoji: String, posts: [String]) {
|
|
self.emoji = emoji
|
|
self.posts = posts
|
|
}
|
|
}
|
|
|
|
/// A node in the reply tree: a fully-resolved post plus its nested children.
|
|
public struct OrgSocialThreadNode: Sendable {
|
|
public let post: OrgSocialPost
|
|
public let children: [OrgSocialThreadNode]
|
|
public let moods: [OrgSocialMood]
|
|
|
|
public init(post: OrgSocialPost, children: [OrgSocialThreadNode] = [], moods: [OrgSocialMood] = []) {
|
|
self.post = post
|
|
self.children = children
|
|
self.moods = moods
|
|
}
|
|
}
|
|
|
|
/// A fully-resolved conversation thread.
|
|
public struct OrgSocialThread: Sendable {
|
|
/// The post the thread was opened from.
|
|
public let focalPost: OrgSocialPost
|
|
/// Ancestor posts, oldest first (may be empty).
|
|
public let parentChain: [OrgSocialPost]
|
|
/// Direct replies to the focal post, each with their own nested children.
|
|
public let replies: [OrgSocialThreadNode]
|
|
/// Emoji reactions on the focal post.
|
|
public let moods: [OrgSocialMood]
|
|
|
|
public init(
|
|
focalPost: OrgSocialPost,
|
|
parentChain: [OrgSocialPost] = [],
|
|
replies: [OrgSocialThreadNode] = [],
|
|
moods: [OrgSocialMood] = []
|
|
) {
|
|
self.focalPost = focalPost
|
|
self.parentChain = parentChain
|
|
self.replies = replies
|
|
self.moods = moods
|
|
}
|
|
|
|
/// Total number of replies (all depths combined).
|
|
public var replyCount: Int {
|
|
replies.reduce(0) { $0 + 1 + countChildren($1) }
|
|
}
|
|
|
|
private func countChildren(_ node: OrgSocialThreadNode) -> Int {
|
|
node.children.reduce(0) { $0 + 1 + countChildren($1) }
|
|
}
|
|
}
|
|
|
|
/// Interaction summary for a post (from /interactions/).
|
|
public struct OrgSocialInteractions: Sendable {
|
|
public let replyCount: Int
|
|
public let reactionCount: Int
|
|
public let boostCount: Int
|
|
public let replyURLs: [String]
|
|
/// Post URLs of the boost posts (one per booster). The feed URL of each
|
|
/// booster is the prefix before `#` — useful for cross-checking "did I
|
|
/// boost this?" without refetching the user's own feed.
|
|
public let boostURLs: [String]
|
|
/// Emoji reactions grouped by emoji, sorted by count descending.
|
|
public let reactions: [OrgSocialMood]
|
|
|
|
public init(
|
|
replyCount: Int,
|
|
reactionCount: Int,
|
|
boostCount: Int,
|
|
replyURLs: [String],
|
|
boostURLs: [String] = [],
|
|
reactions: [OrgSocialMood] = []
|
|
) {
|
|
self.replyCount = replyCount
|
|
self.reactionCount = reactionCount
|
|
self.boostCount = boostCount
|
|
self.replyURLs = replyURLs
|
|
self.boostURLs = boostURLs
|
|
self.reactions = reactions
|
|
}
|
|
|
|
public var hasActivity: Bool { replyCount > 0 || reactionCount > 0 || boostCount > 0 }
|
|
}
|