Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 4 additions & 24 deletions components/dashboard/project/CollaboratorsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import { useContext, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useTranslations } from "next-intl";
import { useCookieUser, useIsPro, useProjectCollaborators, useProjectInvites, useProjectMembership } from "@src/lib/utils/hooks";
import { useCookieUser, useProjectCollaborators, useProjectInvites, useProjectMembership } from "@src/lib/utils/hooks";
import { CookieUser } from "@src/lib/utils/types";
import { ProjectRole } from "../../../src/generated/client/browser";
import { Collaborator, ProjectInvite, ProjectMembershipPayload } from "@src/server/repository/project-repository";
import { Info, Lock } from "lucide-react";
import { Info } from "lucide-react";

import form from "./../../utils/Form.module.css";
import shared from "./ProjectSettings.module.css";
Expand All @@ -16,7 +16,6 @@ import { deleteInvite, inviteCollaborator, kickCollaborator, updateMemberRole }

import * as Roles from "@src/lib/utils/roles";
import { DashboardContext } from "@src/context/DashboardContext";
import Link from "next/link";

const MAX_COLLABORATORS = 5;

Expand All @@ -26,7 +25,6 @@ const CollaboratorsSettings = () => {
const { invites, mutate: mutateInvites } = useProjectInvites(membership?.project.id);
const { collaborators, mutate: mutateCollaborators } = useProjectCollaborators(membership?.project.id);
const { user } = useCookieUser();
const { isPro } = useIsPro();

const slots = useMemo(() => {
if (!membership) return [];
Expand Down Expand Up @@ -103,15 +101,6 @@ const CollaboratorsSettings = () => {
</div>
<p className={shared.helpText}>{t("teamHelp")}</p>

{/* Pro gate banner */}
{!isPro && (
<div className={styles.proGateBanner}>
<Lock size={14} />
<span>{t("proRequired")}</span>
<Link href="/?settings=Profile" className={styles.proGateLink}>{t("upgrade")}</Link>
</div>
)}

{/* Project Collaborators */}
<div className={styles.slotGrid}>
{slots.map((slot) => {
Expand All @@ -137,7 +126,7 @@ const CollaboratorsSettings = () => {
);
case "EMPTY":
return (
<EmptySlot key={slot.key} membership={membership} mutateInvites={mutateInvites} isPro={isPro} />
<EmptySlot key={slot.key} membership={membership} mutateInvites={mutateInvites} />
);
default:
return null;
Expand Down Expand Up @@ -257,10 +246,9 @@ const InviteSlot = ({ data, membership, mutateInvites }: InviteSlotProps) => {
interface EmptySlotProps {
membership: ProjectMembershipPayload;
mutateInvites: () => void;
isPro: boolean;
}

const EmptySlot = ({ membership, mutateInvites, isPro }: EmptySlotProps) => {
const EmptySlot = ({ membership, mutateInvites }: EmptySlotProps) => {
const t = useTranslations("collaborators");
const [email, setEmail] = useState("");

Expand All @@ -273,14 +261,6 @@ const EmptySlot = ({ membership, mutateInvites, isPro }: EmptySlotProps) => {
}
};

if (!isPro) {
return (
<div className={`${styles.slot} ${styles.empty}`}>
<span className={styles.proHint}>{t("proRequiredInvite")}</span>
</div>
);
}

return (
<div className={`${styles.slot} ${styles.empty}`}>
<div className={styles.inviteAction}>
Expand Down
19 changes: 17 additions & 2 deletions src/context/ProjectContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -370,14 +370,29 @@ export const ProjectProvider = ({ children, projectId }: ProjectProviderProps) =
// revalidation of the membership endpoint.
useEffect(() => {
if (!provider) return;
const handler = (newRole: string) => {
const handler = async (newRole: string) => {
setProject((prev) => (prev ? { ...prev, role: newRole as ProjectRole } : prev));
if (project?.project.id) {
try {
const res = await fetch(`/api/projects/${project.project.id}/cloud-token`);
if (res.ok) {
const { token } = (await res.json()) as { token: string };
if (token) {
// Update token silently so future reconnects use the new role
// We don't force reconnect because the DO already updated our active session
await provider.updateToken(token, false);
}
}
} catch (e) {
console.warn("Failed to fetch new token on role change", e);
}
}
};
provider.on("role-changed", handler);
return () => {
provider.off("role-changed", handler);
};
}, [provider]);
}, [provider, project?.project.id]);

const updateScreenplay = useCallback((newScreenplay: Screenplay) => {
setScreenplay(newScreenplay);
Expand Down
22 changes: 15 additions & 7 deletions src/lib/cloud/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,26 @@ export function handleProtocolMessage(room: ProjectRoom, fullMessage: Uint8Array
// apply them, so the rest of the packet stays parseable.
if (isReadOnly) {
decoding.readVarUint8Array(decoder);
console.warn(
`[Room] Dropped doc write from viewer ${session?.userId ?? "?"}`,
);
console.warn(JSON.stringify({
event: "dropped_write_from_viewer",
userId: session?.userId ?? "?"
}));
} else if (subType === SYNC_STEP_2) {
syncProtocol.readSyncStep2(decoder, room.doc, sender);
} else {
syncProtocol.readUpdate(decoder, room.doc, sender);
}
} else {
// Unknown sub-type — bail to avoid mis-aligning the decoder.
console.warn(`[Room] Unknown sync sub-message type: ${subType}`);
console.warn(JSON.stringify({
event: "unknown_sync_sub_message_type",
subType
}));
break;
}
}
} catch (e) {
console.error("[Room] Error reading sync message:", e);
console.error(JSON.stringify({ event: "error_reading_sync_message", error: String(e) }));
}

// If there's a response to send (e.g., SyncStep2 in response to SyncStep1),
Expand Down Expand Up @@ -96,11 +100,15 @@ export function handleProtocolMessage(room: ProjectRoom, fullMessage: Uint8Array
break;

default:
console.warn(`[Room] Unknown message type: ${messageType}`);
console.warn(JSON.stringify({ event: "unknown_message_type", messageType }));
break;
}
} catch (e) {
console.error(`[Room] Protocol error for message type ${messageType}:`, e);
console.error(JSON.stringify({
event: "protocol_error",
messageType,
error: String(e)
}));
// For non-awareness messages, still try to broadcast (might be important)
if (messageType !== 1) {
room.broadcast(fullMessage, sender);
Expand Down
Loading
Loading