Files
andros b8114da874 Use /interactions/ actor URLs: boost count, tappable reactions, voter hosts, cross-check
/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.
2026-04-22 12:43:16 +02:00

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 }
}