Fix truth voting, chat updates, and mobile contact dropdown

- Fix truth voting: add error logging, prevent duplicate event listeners,
  guard against concurrent votes
- Add real-time friends list updates in chat via custom event
- Make mobile contact dropdown narrower (260px) and stay on right side
- Add data-vote-bound attribute to prevent duplicate vote handlers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sociowire Dev 2026-01-12 20:35:29 +00:00
parent 35da962800
commit fa8134fd41
5 changed files with 54 additions and 14 deletions

View File

@ -902,6 +902,13 @@ export async function fetchFriendRequests() {
}
}
// Emit event when friends list changes
function emitFriendsChanged() {
try {
window.dispatchEvent(new CustomEvent("sociowire:friends-changed"));
} catch {}
}
export async function sendFriendRequest(username) {
try {
const res = await fetch(`${apiBase()}/friends/request`, {
@ -911,6 +918,7 @@ export async function sendFriendRequest(username) {
body: JSON.stringify({ username }),
});
const data = await res.json().catch(() => ({}));
if (res.ok) emitFriendsChanged();
return { ok: res.ok, ...data };
} catch {
return { ok: false };
@ -926,6 +934,7 @@ export async function acceptFriendRequest(username) {
body: JSON.stringify({ username }),
});
const data = await res.json().catch(() => ({}));
if (res.ok) emitFriendsChanged();
return { ok: res.ok, ...data };
} catch {
return { ok: false };
@ -941,6 +950,7 @@ export async function rejectFriendRequest(username) {
body: JSON.stringify({ username }),
});
const data = await res.json().catch(() => ({}));
if (res.ok) emitFriendsChanged();
return { ok: res.ok, ...data };
} catch {
return { ok: false };
@ -956,6 +966,7 @@ export async function cancelFriendRequest(username) {
body: JSON.stringify({ username }),
});
const data = await res.json().catch(() => ({}));
if (res.ok) emitFriendsChanged();
return { ok: res.ok, ...data };
} catch {
return { ok: false };
@ -971,6 +982,7 @@ export async function removeFriend(username) {
body: JSON.stringify({ username }),
});
const data = await res.json().catch(() => ({}));
if (res.ok) emitFriendsChanged();
return { ok: res.ok, ...data };
} catch {
return { ok: false };

View File

@ -432,8 +432,8 @@ body[data-theme="light"] .contacts-empty {
.contacts-dropdown {
right: 8px;
left: 8px;
width: auto;
left: auto;
width: 260px;
bottom: 70px;
}

View File

@ -199,6 +199,15 @@ export default function ChatContainer() {
};
loadFriends();
// Listen for friend changes to refresh the list
const handleFriendsChanged = () => {
loadFriends();
};
window.addEventListener("sociowire:friends-changed", handleFriendsChanged);
return () => {
window.removeEventListener("sociowire:friends-changed", handleFriendsChanged);
};
}, [authenticated]);
// Load conversations to get initial unread counts

View File

@ -194,25 +194,29 @@ function updateTruth(modalWrap, post) {
if (label) label.textContent = `${score}`;
}
function updateTruthVoteUI(modalWrap, stats) {
function updateTruthVoteUI(modalWrap, stats, updateScore = false) {
if (!modalWrap || !stats) return;
const aiEl = modalWrap.querySelector("[data-truth-ai]");
const userEl = modalWrap.querySelector("[data-truth-user]");
const countEl = modalWrap.querySelector("[data-truth-count]");
const rail = modalWrap.querySelector(".sw-truth-rail");
const marker = rail ? rail.querySelector(".sw-truth-rail-marker") : null;
const label = rail ? rail.querySelector(".sw-truth-rail-score") : null;
const aiScore = Number.isFinite(stats.ai_score) ? Math.round(stats.ai_score) : stats.AIScore;
const userScore = Number.isFinite(stats.user_score) ? Math.round(stats.user_score) : stats.UserScore;
const truthScoreValue = Number.isFinite(stats.truth_score) ? Math.round(stats.truth_score) : stats.TruthScore;
const totalVotes = Number.isFinite(stats.total_votes) ? stats.total_votes : stats.TotalVotes;
if (aiEl) aiEl.textContent = Number.isFinite(aiScore) ? String(aiScore) : "—";
if (userEl) userEl.textContent = Number.isFinite(userScore) ? String(userScore) : "—";
if (countEl) countEl.textContent = `${totalVotes || 0} votes`;
// Only update the score display if explicitly requested (e.g., after user votes)
if (updateScore) {
const rail = modalWrap.querySelector(".sw-truth-rail");
const marker = rail ? rail.querySelector(".sw-truth-rail-marker") : null;
const label = rail ? rail.querySelector(".sw-truth-rail-score") : null;
const truthScoreValue = Number.isFinite(stats.truth_score) ? Math.round(stats.truth_score) : stats.TruthScore;
if (marker && Number.isFinite(truthScoreValue)) marker.style.top = `${100 - truthScoreValue}%`;
if (label && Number.isFinite(truthScoreValue)) label.textContent = `${truthScoreValue}`;
}
const vote = (stats.user_vote || stats.UserVote || "").toString();
const btns = modalWrap.querySelectorAll(".sw-truth-vote-btn");
@ -1881,7 +1885,9 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
});
}
if (postID > 0) {
// Only refetch if initial data is missing critical fields
const hasCompleteData = postData?.title && (postData?.published_at || postData?.created_at);
if (postID > 0 && !hasCompleteData) {
fetchPostById(postID).then((fresh) => {
if (!fresh) return;
postData = { ...postData, ...fresh };
@ -1936,18 +1942,29 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
}
const rail = modalWrap.querySelector(".sw-truth-rail");
if (rail && postID > 0) {
if (rail && postID > 0 && !rail.hasAttribute("data-vote-bound")) {
rail.setAttribute("data-vote-bound", "1");
const doVote = async (vote) => {
if (!isAuthed()) {
requestAuth("Please sign in or create a free account to vote on truth.");
return;
}
if (rail.classList.contains("sw-truth-vote-disabled")) {
return; // Already voting
}
try {
rail.classList.add("sw-truth-vote-disabled");
const stats = await votePostTruth(postID, vote);
if (stats) updateTruthVoteUI(modalWrap, stats);
} catch {}
if (stats) {
updateTruthVoteUI(modalWrap, stats, true);
} else {
console.warn("votePostTruth returned no stats for post", postID);
}
} catch (err) {
console.error("Truth vote error:", err);
} finally {
rail.classList.remove("sw-truth-vote-disabled");
}
};
const voteUp = rail.querySelector(".sw-truth-rail-btn-up");
const voteDown = rail.querySelector(".sw-truth-rail-btn-down");

View File

@ -737,6 +737,7 @@ body[data-theme="blue"] .sw-centered-modal .post-card.sw-expanded-shell{
background:#0b1220;
border:2px solid #fff;
box-shadow: 0 4px 10px rgba(0,0,0,0.35);
top: 35%;
}
.sw-truth-rail-score{
@ -750,6 +751,7 @@ body[data-theme="blue"] .sw-centered-modal .post-card.sw-expanded-shell{
margin: 0 auto;
position: relative;
left: -6px;
transition: opacity 0.2s ease-out;
}
.sw-truth-rail-btn{