mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-10 23:01:10 +00:00
Merge pull request #1623 from PR0M3TH3AN/ai/refactor-reactions-20241026-2487608613137550346
Refactor reaction logic to ReactionController
This commit is contained in:
3 files changed
+564
-326
No files matched your search
@@ -0,0 +1,371 @@
|
||||
import { devLogger } from "../utils/logger.js";
|
||||
import { pointerArrayToKey } from "../utils/pointer.js";
|
||||
|
||||
export default class ReactionController {
|
||||
constructor({ services = {}, ui = {}, state = {}, callbacks = {} } = {}) {
|
||||
this.services = {
|
||||
reactionCounter: services.reactionCounter,
|
||||
};
|
||||
this.ui = {
|
||||
getVideoModal: ui.getVideoModal || (() => null),
|
||||
showError: ui.showError || (() => {}),
|
||||
};
|
||||
this.state = {
|
||||
getCurrentVideo: state.getCurrentVideo || (() => null),
|
||||
getCurrentVideoPointer: state.getCurrentVideoPointer || (() => null),
|
||||
getCurrentVideoPointerKey: state.getCurrentVideoPointerKey || (() => null),
|
||||
};
|
||||
this.callbacks = {
|
||||
isUserLoggedIn: callbacks.isUserLoggedIn || (() => false),
|
||||
normalizeHexPubkey: callbacks.normalizeHexPubkey || ((val) => val),
|
||||
getPubkey: callbacks.getPubkey || (() => null),
|
||||
};
|
||||
|
||||
this.reactionState = {
|
||||
counts: { "+": 0, "-": 0 },
|
||||
total: 0,
|
||||
userReaction: "",
|
||||
};
|
||||
this.unsub = null;
|
||||
this.pointerKey = null;
|
||||
}
|
||||
|
||||
normalizeReactionCount(value) {
|
||||
if (Number.isFinite(value)) {
|
||||
return Math.max(0, Math.round(Number(value)));
|
||||
}
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? Math.max(0, Math.round(parsed)) : 0;
|
||||
}
|
||||
|
||||
resetState() {
|
||||
this.reactionState = {
|
||||
counts: { "+": 0, "-": 0 },
|
||||
total: 0,
|
||||
userReaction: "",
|
||||
};
|
||||
const videoModal = this.ui.getVideoModal();
|
||||
if (videoModal?.updateReactionSummary) {
|
||||
videoModal.updateReactionSummary({
|
||||
total: 0,
|
||||
counts: { "+": 0, "-": 0 },
|
||||
userReaction: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribe() {
|
||||
if (typeof this.unsub === "function") {
|
||||
try {
|
||||
this.unsub();
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[reaction] Failed to tear down modal subscription:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
this.unsub = null;
|
||||
this.pointerKey = null;
|
||||
this.resetState();
|
||||
}
|
||||
|
||||
subscribe(pointer, pointerKey) {
|
||||
const videoModal = this.ui.getVideoModal();
|
||||
if (!videoModal?.updateReactionSummary) {
|
||||
this.unsubscribe();
|
||||
return;
|
||||
}
|
||||
|
||||
this.unsubscribe();
|
||||
|
||||
if (!pointer || !pointerKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const normalizedUser = this.callbacks.normalizeHexPubkey(this.callbacks.getPubkey());
|
||||
const unsubscribe = this.services.reactionCounter.subscribe(pointer, (snapshot) => {
|
||||
const counts = { ...this.reactionState.counts };
|
||||
if (snapshot?.counts && typeof snapshot.counts === "object") {
|
||||
for (const [key, value] of Object.entries(snapshot.counts)) {
|
||||
counts[key] = this.normalizeReactionCount(value);
|
||||
}
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(counts, "+")) {
|
||||
counts["+"] = 0;
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(counts, "-")) {
|
||||
counts["-"] = 0;
|
||||
}
|
||||
|
||||
let total = Number.isFinite(snapshot?.total)
|
||||
? Math.max(0, Number(snapshot.total))
|
||||
: 0;
|
||||
if (!Number.isFinite(total) || total === 0) {
|
||||
total = 0;
|
||||
for (const value of Object.values(counts)) {
|
||||
total += this.normalizeReactionCount(value);
|
||||
}
|
||||
}
|
||||
|
||||
let userReaction = "";
|
||||
if (normalizedUser && snapshot?.reactions) {
|
||||
const record = snapshot.reactions[normalizedUser] || null;
|
||||
if (record && typeof record.content === "string") {
|
||||
userReaction =
|
||||
record.content === "+"
|
||||
? "+"
|
||||
: record.content === "-"
|
||||
? "-"
|
||||
: "";
|
||||
}
|
||||
}
|
||||
|
||||
this.reactionState = {
|
||||
counts,
|
||||
total,
|
||||
userReaction,
|
||||
};
|
||||
|
||||
const currentModal = this.ui.getVideoModal();
|
||||
if (currentModal?.updateReactionSummary) {
|
||||
currentModal.updateReactionSummary({
|
||||
total,
|
||||
counts,
|
||||
userReaction,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.pointerKey = pointerKey;
|
||||
this.unsub = () => {
|
||||
try {
|
||||
unsubscribe?.();
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[reaction] Failed to tear down modal subscription:",
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
this.unsub = null;
|
||||
this.pointerKey = null;
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[reaction] Failed to subscribe modal reaction counter:",
|
||||
error,
|
||||
);
|
||||
this.resetState();
|
||||
}
|
||||
}
|
||||
|
||||
applyOptimisticUpdate(nextReaction) {
|
||||
if (nextReaction !== "+" && nextReaction !== "-") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const previousCounts = {
|
||||
...(this.reactionState?.counts || {}),
|
||||
};
|
||||
const previousTotalValue = Number.isFinite(this.reactionState?.total)
|
||||
? Math.max(0, Number(this.reactionState.total))
|
||||
: null;
|
||||
const previousReaction = this.reactionState?.userReaction || "";
|
||||
|
||||
const likeBefore = this.normalizeReactionCount(previousCounts["+"]);
|
||||
const dislikeBefore = this.normalizeReactionCount(previousCounts["-"]);
|
||||
const otherCounts = {};
|
||||
for (const [key, value] of Object.entries(previousCounts)) {
|
||||
if (key === "+" || key === "-") {
|
||||
continue;
|
||||
}
|
||||
otherCounts[key] = this.normalizeReactionCount(value);
|
||||
}
|
||||
|
||||
let likeCount = likeBefore;
|
||||
let dislikeCount = dislikeBefore;
|
||||
|
||||
if (previousReaction === "+") {
|
||||
likeCount = Math.max(0, likeCount - 1);
|
||||
} else if (previousReaction === "-") {
|
||||
dislikeCount = Math.max(0, dislikeCount - 1);
|
||||
}
|
||||
|
||||
if (nextReaction === "+") {
|
||||
likeCount += 1;
|
||||
} else if (nextReaction === "-") {
|
||||
dislikeCount += 1;
|
||||
}
|
||||
|
||||
const updatedCounts = {
|
||||
...previousCounts,
|
||||
"+": likeCount,
|
||||
"-": dislikeCount,
|
||||
};
|
||||
|
||||
let updatedTotal = likeCount + dislikeCount;
|
||||
for (const value of Object.values(otherCounts)) {
|
||||
updatedTotal += this.normalizeReactionCount(value);
|
||||
}
|
||||
|
||||
this.reactionState = {
|
||||
counts: updatedCounts,
|
||||
total: updatedTotal,
|
||||
userReaction: nextReaction,
|
||||
};
|
||||
|
||||
const videoModal = this.ui.getVideoModal();
|
||||
if (videoModal?.updateReactionSummary) {
|
||||
videoModal.updateReactionSummary({
|
||||
total: updatedTotal,
|
||||
counts: updatedCounts,
|
||||
userReaction: nextReaction,
|
||||
});
|
||||
}
|
||||
|
||||
const fallbackPreviousTotal = Number.isFinite(previousTotalValue)
|
||||
? previousTotalValue
|
||||
: likeBefore +
|
||||
dislikeBefore +
|
||||
Object.values(otherCounts).reduce(
|
||||
(sum, value) => sum + this.normalizeReactionCount(value),
|
||||
0
|
||||
);
|
||||
|
||||
return {
|
||||
counts: previousCounts,
|
||||
total: fallbackPreviousTotal,
|
||||
userReaction: previousReaction,
|
||||
};
|
||||
}
|
||||
|
||||
restoreSnapshot(snapshot) {
|
||||
if (!snapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const countsInput =
|
||||
snapshot.counts && typeof snapshot.counts === "object"
|
||||
? snapshot.counts
|
||||
: {};
|
||||
const counts = { ...countsInput };
|
||||
for (const [key, value] of Object.entries(counts)) {
|
||||
counts[key] = this.normalizeReactionCount(value);
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(counts, "+")) {
|
||||
counts["+"] = 0;
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(counts, "-")) {
|
||||
counts["-"] = 0;
|
||||
}
|
||||
|
||||
let total = Number.isFinite(snapshot.total)
|
||||
? Math.max(0, Number(snapshot.total))
|
||||
: 0;
|
||||
if (!Number.isFinite(total) || total === 0) {
|
||||
total = 0;
|
||||
for (const value of Object.values(counts)) {
|
||||
total += this.normalizeReactionCount(value);
|
||||
}
|
||||
}
|
||||
|
||||
const userReaction =
|
||||
snapshot.userReaction === "+"
|
||||
? "+"
|
||||
: snapshot.userReaction === "-"
|
||||
? "-"
|
||||
: "";
|
||||
|
||||
this.reactionState = {
|
||||
counts,
|
||||
total,
|
||||
userReaction,
|
||||
};
|
||||
|
||||
const videoModal = this.ui.getVideoModal();
|
||||
if (videoModal?.updateReactionSummary) {
|
||||
videoModal.updateReactionSummary({
|
||||
total,
|
||||
counts,
|
||||
userReaction,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async handleReaction(detail = {}) {
|
||||
const videoModal = this.ui.getVideoModal();
|
||||
if (!videoModal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const requestedReaction =
|
||||
typeof detail.reaction === "string" ? detail.reaction : "";
|
||||
const normalizedReaction =
|
||||
requestedReaction === "+"
|
||||
? "+"
|
||||
: requestedReaction === "-"
|
||||
? "-"
|
||||
: "";
|
||||
|
||||
if (!normalizedReaction) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousReaction = this.reactionState?.userReaction || "";
|
||||
const pointer = this.state.getCurrentVideoPointer();
|
||||
const pointerKey = this.state.getCurrentVideoPointerKey() || (pointer ? pointerArrayToKey(pointer) : null);
|
||||
|
||||
if (!pointer || !pointerKey) {
|
||||
if (videoModal) {
|
||||
videoModal.setUserReaction(previousReaction);
|
||||
}
|
||||
devLogger.info(
|
||||
"[reaction] Ignoring reaction request until modal pointer is available.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (normalizedReaction === previousReaction) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.callbacks.isUserLoggedIn()) {
|
||||
this.ui.showError("Please login to react to videos.");
|
||||
videoModal.setUserReaction(previousReaction);
|
||||
return;
|
||||
}
|
||||
|
||||
let rollbackSnapshot = null;
|
||||
try {
|
||||
rollbackSnapshot = this.applyOptimisticUpdate(
|
||||
normalizedReaction
|
||||
);
|
||||
} catch (error) {
|
||||
devLogger.warn("[reaction] Failed to apply optimistic reaction state:", error);
|
||||
}
|
||||
|
||||
try {
|
||||
const currentVideo = this.state.getCurrentVideo();
|
||||
const result = await this.services.reactionCounter.publish(pointer, {
|
||||
content: normalizedReaction,
|
||||
video: currentVideo,
|
||||
currentVideoPubkey: currentVideo?.pubkey,
|
||||
pointerKey,
|
||||
});
|
||||
|
||||
if (!result?.ok) {
|
||||
if (rollbackSnapshot) {
|
||||
this.restoreSnapshot(rollbackSnapshot);
|
||||
}
|
||||
this.ui.showError("Failed to send reaction. Please try again.");
|
||||
}
|
||||
} catch (error) {
|
||||
devLogger.warn("[reaction] Failed to publish reaction:", error);
|
||||
if (rollbackSnapshot) {
|
||||
this.restoreSnapshot(rollbackSnapshot);
|
||||
}
|
||||
this.ui.showError("Failed to send reaction. Please try again.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user