Merge public-gh/master into review/pr-162
This commit is contained in:
@@ -12,8 +12,7 @@ import type { Company } from "@paperclipai/shared";
|
||||
import { companiesApi } from "../api/companies";
|
||||
import { ApiError } from "../api/client";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
|
||||
type CompanySelectionSource = "manual" | "route_sync" | "bootstrap";
|
||||
import type { CompanySelectionSource } from "../lib/company-selection";
|
||||
type CompanySelectionOptions = { source?: CompanySelectionSource };
|
||||
|
||||
interface CompanyContextValue {
|
||||
|
||||
@@ -5,6 +5,9 @@ interface NewIssueDefaults {
|
||||
priority?: string;
|
||||
projectId?: string;
|
||||
assigneeAgentId?: string;
|
||||
assigneeUserId?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface NewGoalDefaults {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, type ReactNode } from "react";
|
||||
import { useQueryClient, type QueryClient } from "@tanstack/react-query";
|
||||
import { useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query";
|
||||
import type { Agent, Issue, LiveEvent } from "@paperclipai/shared";
|
||||
import { authApi } from "../api/auth";
|
||||
import { useCompany } from "./CompanyContext";
|
||||
import type { ToastInput } from "./ToastContext";
|
||||
import { useToast } from "./ToastContext";
|
||||
@@ -152,6 +153,7 @@ function buildActivityToast(
|
||||
queryClient: QueryClient,
|
||||
companyId: string,
|
||||
payload: Record<string, unknown>,
|
||||
currentActor: { userId: string | null; agentId: string | null },
|
||||
): ToastInput | null {
|
||||
const entityType = readString(payload.entityType);
|
||||
const entityId = readString(payload.entityId);
|
||||
@@ -166,6 +168,10 @@ function buildActivityToast(
|
||||
|
||||
const issue = resolveIssueToastContext(queryClient, companyId, entityId, details);
|
||||
const actor = resolveActorLabel(queryClient, companyId, actorType, actorId);
|
||||
const isSelfActivity =
|
||||
(actorType === "user" && !!currentActor.userId && actorId === currentActor.userId) ||
|
||||
(actorType === "agent" && !!currentActor.agentId && actorId === currentActor.agentId);
|
||||
if (isSelfActivity) return null;
|
||||
|
||||
if (action === "issue.created") {
|
||||
return {
|
||||
@@ -178,8 +184,8 @@ function buildActivityToast(
|
||||
}
|
||||
|
||||
if (action === "issue.updated") {
|
||||
if (details?.reopened === true && readString(details.source) === "comment") {
|
||||
// Reopen-via-comment emits a paired comment event; show one combined toast on the comment event.
|
||||
if (readString(details?.source) === "comment") {
|
||||
// Comment-driven updates emit a paired comment event; show one combined toast on the comment event.
|
||||
return null;
|
||||
}
|
||||
const changeDesc = describeIssueUpdate(details);
|
||||
@@ -202,13 +208,18 @@ function buildActivityToast(
|
||||
const commentId = readString(details?.commentId);
|
||||
const bodySnippet = readString(details?.bodySnippet);
|
||||
const reopened = details?.reopened === true;
|
||||
const updated = details?.updated === true;
|
||||
const reopenedFrom = readString(details?.reopenedFrom);
|
||||
const reopenedLabel = reopened
|
||||
? reopenedFrom
|
||||
? `reopened from ${reopenedFrom.replace(/_/g, " ")}`
|
||||
: "reopened"
|
||||
: null;
|
||||
const title = reopened ? `${actor} reopened and commented on ${issue.ref}` : `${actor} commented on ${issue.ref}`;
|
||||
const title = reopened
|
||||
? `${actor} reopened and commented on ${issue.ref}`
|
||||
: updated
|
||||
? `${actor} commented and updated ${issue.ref}`
|
||||
: `${actor} commented on ${issue.ref}`;
|
||||
const body = bodySnippet
|
||||
? reopenedLabel
|
||||
? `${reopenedLabel} - ${bodySnippet.replace(/^#+\s*/m, "").replace(/\n/g, " ")}`
|
||||
@@ -245,7 +256,7 @@ function buildJoinRequestToast(
|
||||
title: `${label} wants to join`,
|
||||
body: "A new join request is waiting for approval.",
|
||||
tone: "info",
|
||||
action: { label: "View inbox", href: "/inbox/new" },
|
||||
action: { label: "View inbox", href: "/inbox/unread" },
|
||||
dedupeKey: `join-request:${entityId}`,
|
||||
};
|
||||
}
|
||||
@@ -358,6 +369,9 @@ function invalidateActivityQueries(
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.comments(ref) });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.activity(ref) });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.runs(ref) });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.documents(ref) });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.attachments(ref) });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.approvals(ref) });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.liveRuns(ref) });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.activeRun(ref) });
|
||||
}
|
||||
@@ -448,6 +462,7 @@ function handleLiveEvent(
|
||||
event: LiveEvent,
|
||||
pushToast: (toast: ToastInput) => string | null,
|
||||
gate: ToastGate,
|
||||
currentActor: { userId: string | null; agentId: string | null },
|
||||
) {
|
||||
if (event.companyId !== expectedCompanyId) return;
|
||||
|
||||
@@ -485,7 +500,7 @@ function handleLiveEvent(
|
||||
invalidateActivityQueries(queryClient, expectedCompanyId, payload);
|
||||
const action = readString(payload.action);
|
||||
const toast =
|
||||
buildActivityToast(queryClient, expectedCompanyId, payload) ??
|
||||
buildActivityToast(queryClient, expectedCompanyId, payload, currentActor) ??
|
||||
buildJoinRequestToast(payload);
|
||||
if (toast) gatedPushToast(gate, pushToast, `activity:${action ?? "unknown"}`, toast);
|
||||
}
|
||||
@@ -496,6 +511,12 @@ export function LiveUpdatesProvider({ children }: { children: ReactNode }) {
|
||||
const queryClient = useQueryClient();
|
||||
const { pushToast } = useToast();
|
||||
const gateRef = useRef<ToastGate>({ cooldownHits: new Map(), suppressUntil: 0 });
|
||||
const { data: session } = useQuery({
|
||||
queryKey: queryKeys.auth.session,
|
||||
queryFn: () => authApi.getSession(),
|
||||
retry: false,
|
||||
});
|
||||
const currentUserId = session?.user?.id ?? session?.session?.userId ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedCompanyId) return;
|
||||
@@ -541,7 +562,10 @@ export function LiveUpdatesProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as LiveEvent;
|
||||
handleLiveEvent(queryClient, selectedCompanyId, parsed, pushToast, gateRef.current);
|
||||
handleLiveEvent(queryClient, selectedCompanyId, parsed, pushToast, gateRef.current, {
|
||||
userId: currentUserId,
|
||||
agentId: null,
|
||||
});
|
||||
} catch {
|
||||
// Ignore non-JSON payloads.
|
||||
}
|
||||
@@ -570,7 +594,7 @@ export function LiveUpdatesProvider({ children }: { children: ReactNode }) {
|
||||
socket.close(1000, "provider_unmount");
|
||||
}
|
||||
};
|
||||
}, [queryClient, selectedCompanyId, pushToast]);
|
||||
}, [queryClient, selectedCompanyId, pushToast, currentUserId]);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user