154 lines
5.4 KiB
TypeScript
154 lines
5.4 KiB
TypeScript
import { useEffect } from "react";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import { supabase } from "../lib/supabase";
|
|
import type { RealtimeChannel } from "@supabase/supabase-js";
|
|
import type { CommunityPost } from "../stores/community";
|
|
|
|
/**
|
|
* Realtime-Subscription für die Community-Feed-Page.
|
|
* Lauscht auf:
|
|
* - INSERT auf community_posts → invalidiert die Feed-Query (frischer Refetch)
|
|
* - UPDATE auf community_posts → patcht likes/comments-Counts inline
|
|
* - UPDATE auf domain_submissions → patcht domain_vote-Posts mit neuem Status
|
|
* - UPDATE auf game_challenges → patcht challenge-Status
|
|
*
|
|
* Pendant zum Nuxt-`communityStore.startRealtime()` aus apps/rebreak/.
|
|
*/
|
|
export function useCommunityRealtime(enabled: boolean = true) {
|
|
const queryClient = useQueryClient();
|
|
|
|
useEffect(() => {
|
|
if (!enabled) return;
|
|
let channel: RealtimeChannel | null = null;
|
|
let cancelled = false;
|
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
async function subscribe() {
|
|
const { data } = await supabase.auth.getSession();
|
|
const session = data.session;
|
|
if (!session?.access_token) return;
|
|
if (cancelled) return;
|
|
|
|
supabase.realtime.setAuth(session.access_token);
|
|
const myId = session.user.id;
|
|
|
|
channel = supabase
|
|
.channel(`community:posts:${Date.now()}`)
|
|
.on(
|
|
"postgres_changes",
|
|
{ event: "INSERT", schema: "rebreak", table: "community_posts" },
|
|
(payload: any) => {
|
|
const r = payload.new;
|
|
if (r.user_id === myId) return; // eigene Posts schon optimistisch hinzugefügt
|
|
if (r.is_moderated) return;
|
|
// Einfacher als Detail-Fetch: alle Feed-Queries invalidieren
|
|
queryClient.invalidateQueries({ queryKey: ["community-posts"] });
|
|
},
|
|
)
|
|
.on(
|
|
"postgres_changes",
|
|
{ event: "UPDATE", schema: "rebreak", table: "community_posts" },
|
|
(payload: any) => {
|
|
const r = payload.new;
|
|
patchPostInAllQueries(queryClient, r.id, (p) => ({
|
|
...p,
|
|
likesCount: r.likes_count ?? p.likesCount,
|
|
dislikesCount: r.dislikes_count ?? p.dislikesCount,
|
|
commentsCount: r.comments_count ?? p.commentsCount,
|
|
repostsCount: r.reposts_count ?? p.repostsCount,
|
|
}));
|
|
},
|
|
)
|
|
.on(
|
|
"postgres_changes",
|
|
{ event: "UPDATE", schema: "rebreak", table: "domain_submissions" },
|
|
(payload: any) => {
|
|
const r = payload.new;
|
|
if (
|
|
r.status !== "approved" &&
|
|
r.status !== "rejected" &&
|
|
r.status !== "in_review"
|
|
) {
|
|
return;
|
|
}
|
|
patchPostInAllQueries(queryClient, null, (p) => {
|
|
if (p.submission?.domain == null) return p;
|
|
// Wir kennen die submissionId nicht direkt am Post, also matche per domain.
|
|
// Realistisch unique pro user_id, aber Feed enthält Post mit submission-Objekt.
|
|
// Wenn der Post diese submission referenziert, patchen.
|
|
if (!p.submission || (p as any).submissionId !== r.id) {
|
|
// Falls du submissionId an Post-Schema hängst, hier nutzen.
|
|
// Solange nicht: invalidate fallback unten.
|
|
return p;
|
|
}
|
|
return {
|
|
...p,
|
|
submission: {
|
|
...p.submission,
|
|
status: r.status,
|
|
yesVotes: r.yes_votes ?? p.submission.yesVotes,
|
|
noVotes: r.no_votes ?? p.submission.noVotes,
|
|
reviewedAt: r.reviewed_at ?? p.submission.reviewedAt,
|
|
},
|
|
};
|
|
});
|
|
// Sicherheitshalber auch invalidieren — domain_vote ist selten genug.
|
|
queryClient.invalidateQueries({ queryKey: ["community-posts"] });
|
|
},
|
|
)
|
|
.on(
|
|
"postgres_changes",
|
|
{ event: "UPDATE", schema: "rebreak", table: "game_challenges" },
|
|
(payload: any) => {
|
|
const r = payload.new;
|
|
patchPostInAllQueries(queryClient, null, (p) =>
|
|
p.challengeId === r.id ? { ...p, challengeStatus: r.status } : p,
|
|
);
|
|
},
|
|
)
|
|
.subscribe((status) => {
|
|
if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
|
|
cleanup();
|
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
reconnectTimer = setTimeout(() => {
|
|
if (!cancelled) subscribe();
|
|
}, 3000);
|
|
}
|
|
});
|
|
}
|
|
|
|
function cleanup() {
|
|
if (channel) {
|
|
supabase.removeChannel(channel);
|
|
channel = null;
|
|
}
|
|
}
|
|
|
|
subscribe();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
cleanup();
|
|
};
|
|
}, [enabled, queryClient]);
|
|
}
|
|
|
|
function patchPostInAllQueries(
|
|
queryClient: ReturnType<typeof useQueryClient>,
|
|
postId: string | null,
|
|
patcher: (p: CommunityPost) => CommunityPost,
|
|
) {
|
|
const queries = queryClient.getQueriesData<CommunityPost[]>({
|
|
queryKey: ["community-posts"],
|
|
});
|
|
for (const [key, data] of queries) {
|
|
if (!Array.isArray(data)) continue;
|
|
const next = data.map((p) => {
|
|
if (postId !== null && p.id !== postId) return p;
|
|
return patcher(p);
|
|
});
|
|
queryClient.setQueryData(key, next);
|
|
}
|
|
}
|