Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/Shared/Localization/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"Send" = "发送";
"Quote" = "引用";
"Vote Up" = "点赞";
"Vote Failed" = "操作失败";
"Edit" = "编辑";
"Edit Signature" = "修改签名";
"Share" = "分享";
Expand Down
23 changes: 23 additions & 0 deletions app/Shared/Models/VotesModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,29 @@ import SwiftUI
class VotesModel: ObservableObject {
typealias Vote = (state: VoteState, delta: Int32)

/// Predict the resulting vote after applying `operation`, mirroring NGA's
/// toggle semantics so optimistic updates match what the server returns:
/// tapping the current state again clears it; switching across up/down moves
/// by 2. Used to update the UI instantly before the network round-trip.
static func predictVote(from current: Vote, operation: PostVoteRequest.Operation) -> Vote {
switch (operation, current.state) {
case (.upvote, .up): // toggle off
return (state: .none, delta: current.delta - 1)
case (.upvote, .down): // flip down -> up
return (state: .up, delta: current.delta + 2)
case (.upvote, _): // none -> up
return (state: .up, delta: current.delta + 1)
case (.downvote, .down): // toggle off
return (state: .none, delta: current.delta + 1)
case (.downvote, .up): // flip up -> down
return (state: .down, delta: current.delta - 2)
case (.downvote, _): // none -> down
return (state: .down, delta: current.delta - 1)
default: // unknown operation (e.g. UNRECOGNIZED): no change
return current
}
}

// TODO: check performance cost here
@Published private var votes = [PostId: Vote]()

Expand Down
31 changes: 23 additions & 8 deletions app/Shared/Views/PostRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -367,23 +367,38 @@ struct PostRowView: View {
return
}

// Optimistically update the UI immediately, then send the request in the
// background and reconcile with the server's authoritative result. On
// failure, roll back to the previous state.
let previous = vote
let predicted = VotesModel.predictVote(from: previous, operation: operation)

withAnimation {
vote = predicted
#if os(iOS)
if vote.state != .none {
HapticUtils.play(style: .light)
}
#endif
}

logicCallAsync(.postVote(.with {
$0.postID = post.id
$0.operation = operation
})) { (response: PostVoteResponse) in
}), errorToastModel: nil) { (response: PostVoteResponse) in
if !response.hasError {
// Reconcile delta against the pre-vote baseline so the server value wins
// regardless of our prediction.
withAnimation {
vote.state = response.state
vote.delta += response.delta
#if os(iOS)
if vote.state != .none {
HapticUtils.play(style: .light)
}
#endif
vote.delta = previous.delta + response.delta
}
Comment on lines +370 to 395
} else {
// not used
withAnimation { vote = previous }
}
} onError: { _ in
withAnimation { vote = previous }
ToastModel.showAuto(.error("Vote Failed"))
}
Comment on lines 385 to 402
}

Expand Down