ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: presence автора и унификация карточек
This commit is contained in:
@@ -6,12 +6,15 @@
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { LIVE_BASE_PATH, LIVE_BASE_URL } from "@plane/constants";
|
||||
import { applyPresenceRealtimeEvent } from "@/hooks/use-user-presence";
|
||||
|
||||
type TExternalContourRealtimeEvent = {
|
||||
event_id?: string;
|
||||
type?: string;
|
||||
workspace_slug?: string;
|
||||
project_id?: string;
|
||||
user_id?: string;
|
||||
online_user_ids?: string[];
|
||||
};
|
||||
|
||||
const SYNC_DEBOUNCE_MS = 350;
|
||||
@@ -101,6 +104,10 @@ export const useExternalContoursRealtimeEvents = (
|
||||
}
|
||||
|
||||
if (event.type === "issue.stream.ready") return;
|
||||
if (event.type?.startsWith("presence.")) {
|
||||
applyPresenceRealtimeEvent(event);
|
||||
return;
|
||||
}
|
||||
if (event.workspace_slug && event.workspace_slug !== workspaceSlug) return;
|
||||
if (event.project_id && event.project_id !== projectId) return;
|
||||
if (!event.type?.startsWith("external_contour.") && !event.type?.startsWith("issue.")) return;
|
||||
|
||||
@@ -11,16 +11,28 @@ import type { TIssue } from "@plane/types";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { applyPresenceRealtimeEvent } from "@/hooks/use-user-presence";
|
||||
// services
|
||||
import { IssueService } from "@/services/issue";
|
||||
|
||||
type TIssueRealtimeEvent = {
|
||||
event_id: string;
|
||||
type: "issue.created" | "issue.updated" | "issue.deleted" | "issue.stream.ready" | "issue.stream.ping";
|
||||
type:
|
||||
| "issue.created"
|
||||
| "issue.updated"
|
||||
| "issue.deleted"
|
||||
| "issue.stream.ready"
|
||||
| "issue.stream.ping"
|
||||
| "presence.snapshot"
|
||||
| "presence.user.online"
|
||||
| "presence.user.offline"
|
||||
| "presence.user.heartbeat";
|
||||
workspace_slug?: string;
|
||||
project_id?: string;
|
||||
issue_id?: string;
|
||||
updated_at?: string;
|
||||
user_id?: string;
|
||||
online_user_ids?: string[];
|
||||
};
|
||||
|
||||
type TRealtimeIssueStore = {
|
||||
@@ -234,6 +246,10 @@ export const useIssueRealtimeEvents = (storeType: EIssuesStoreType, workspaceSlu
|
||||
}
|
||||
|
||||
if (event.type === "issue.stream.ready") return;
|
||||
if (event.type?.startsWith("presence.")) {
|
||||
applyPresenceRealtimeEvent(event);
|
||||
return;
|
||||
}
|
||||
if (!event.type?.startsWith("issue.")) return;
|
||||
if (event.workspace_slug && event.workspace_slug !== workspaceSlug) return;
|
||||
if (event.project_id && event.project_id !== projectId) return;
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
type TPresenceRealtimeEvent = {
|
||||
type?: string;
|
||||
workspace_slug?: string;
|
||||
user_id?: string;
|
||||
online_user_ids?: string[];
|
||||
};
|
||||
|
||||
const PRESENCE_STALE_MS = 70_000;
|
||||
const PRUNE_INTERVAL_MS = 10_000;
|
||||
|
||||
const presenceByWorkspace = new Map<string, Map<string, number>>();
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
let pruneTimer: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
const getExpiry = () => Date.now() + PRESENCE_STALE_MS;
|
||||
|
||||
const notify = () => {
|
||||
listeners.forEach((listener) => listener());
|
||||
};
|
||||
|
||||
const pruneExpiredPresence = () => {
|
||||
const now = Date.now();
|
||||
let hasChanges = false;
|
||||
|
||||
presenceByWorkspace.forEach((workspacePresence, workspaceSlug) => {
|
||||
workspacePresence.forEach((expiresAt, userId) => {
|
||||
if (expiresAt > now) return;
|
||||
workspacePresence.delete(userId);
|
||||
hasChanges = true;
|
||||
});
|
||||
|
||||
if (workspacePresence.size === 0) presenceByWorkspace.delete(workspaceSlug);
|
||||
});
|
||||
|
||||
if (hasChanges) notify();
|
||||
};
|
||||
|
||||
const subscribePresence = (listener: () => void) => {
|
||||
listeners.add(listener);
|
||||
|
||||
if (!pruneTimer) {
|
||||
pruneTimer = setInterval(pruneExpiredPresence, PRUNE_INTERVAL_MS);
|
||||
}
|
||||
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
|
||||
if (listeners.size === 0 && pruneTimer) {
|
||||
clearInterval(pruneTimer);
|
||||
pruneTimer = undefined;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const getWorkspacePresence = (workspaceSlug: string) => {
|
||||
const existingPresence = presenceByWorkspace.get(workspaceSlug);
|
||||
if (existingPresence) return existingPresence;
|
||||
|
||||
const nextPresence = new Map<string, number>();
|
||||
presenceByWorkspace.set(workspaceSlug, nextPresence);
|
||||
|
||||
return nextPresence;
|
||||
};
|
||||
|
||||
const getPresenceSnapshot = (workspaceSlug?: string, userId?: string) => {
|
||||
if (!workspaceSlug || !userId) return false;
|
||||
|
||||
const workspacePresence = presenceByWorkspace.get(workspaceSlug);
|
||||
const expiresAt = workspacePresence?.get(userId);
|
||||
|
||||
return !!expiresAt && expiresAt > Date.now();
|
||||
};
|
||||
|
||||
export const applyPresenceRealtimeEvent = (event: TPresenceRealtimeEvent) => {
|
||||
if (!event.type?.startsWith("presence.") || !event.workspace_slug) return;
|
||||
|
||||
if (event.type === "presence.snapshot") {
|
||||
const workspacePresence = new Map<string, number>();
|
||||
const expiresAt = getExpiry();
|
||||
|
||||
event.online_user_ids?.forEach((userId) => {
|
||||
if (userId) workspacePresence.set(userId, expiresAt);
|
||||
});
|
||||
|
||||
presenceByWorkspace.set(event.workspace_slug, workspacePresence);
|
||||
notify();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!event.user_id) return;
|
||||
|
||||
const workspacePresence = getWorkspacePresence(event.workspace_slug);
|
||||
|
||||
if (event.type === "presence.user.offline") {
|
||||
workspacePresence.delete(event.user_id);
|
||||
} else {
|
||||
workspacePresence.set(event.user_id, getExpiry());
|
||||
}
|
||||
|
||||
notify();
|
||||
};
|
||||
|
||||
export const usePresenceStatus = (workspaceSlug?: string, userId?: string) =>
|
||||
useSyncExternalStore(
|
||||
subscribePresence,
|
||||
() => getPresenceSnapshot(workspaceSlug, userId),
|
||||
() => false
|
||||
);
|
||||
Reference in New Issue
Block a user