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
2 changes: 2 additions & 0 deletions apps/web/src/collab/collab-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const DISABLED: CollabContextValue = {
syncState: null,
viewerOnly: false,
isOwner: false,
isEffectiveOwner: false,
isSharedNonOwner: false,
ownerDisplayName: null,
ownerRole: null,
downloadPending: false,
Expand Down
44 changes: 44 additions & 0 deletions apps/web/src/collab/useProjectCollab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,22 @@ export interface ProjectCollab {
* re-deriving ownership at the call site.
*/
isOwner: boolean;
/**
* Effective project ownership for UX that must not wait on `/collab/status`.
* True when status confirms the viewer is the single writer **or** the hub
* catalog already names them as owner (issue #99) **or** this tab created
* the project this session. Prefer this over raw `isOwner` for sticky defaults
* (e.g. default chat open/collapsed) that would otherwise latch wrong while
* `ownerMemberId` is still missing from a shared status payload.
*/
isEffectiveOwner: boolean;
/**
* Confirmed: this is a shared team project the viewer did **not** create.
* Requires positive evidence of a different owner (status `ownerMemberId`,
* catalog owner, or a previously confirmed non-owner relationship) — never
* mere `!isOwner` during the status-unknown window.
*/
isSharedNonOwner: boolean;
/**
* Display name of the member who shared this project (its owner), resolved
* server-side from the collab-cloud member directory. Drives the "这是 {owner}
Expand Down Expand Up @@ -296,13 +312,27 @@ export function useProjectCollab(
&& knownCatalog.some(
(entry) => entry.projectId === projectId && entry.ownerMemberId === context.workspaceMemberId,
);
// Catalog names a different member as the single writer — usable before
// `/collab/status` confirms ownerMemberId (mirror of knownOwnedByViewer).
const knownOwnedBySomeoneElse =
knownCatalog !== null
&& Boolean(projectId)
&& context?.workspaceMemberId != null
&& knownCatalog.some(
(entry) =>
entry.projectId === projectId
&& entry.ownerMemberId != null
&& entry.ownerMemberId !== context.workspaceMemberId,
);
// A project this browser tab created moments ago cannot possibly be shared
// yet — see `projectIdsCreatedByViewerThisSession` above. This covers the
// window `knownUnshared` cannot: right after creation the project is not
// (and cannot yet be) in the team catalog at all, so `knownCatalog` loading
// or not loading is irrelevant here.
const createdByViewerThisSession =
Boolean(projectId) && projectIdsCreatedByViewerThisSession.has(projectId as string);
// Status-confirmed ownership OR catalog/session shortcuts (issue #99 path).
const isEffectiveOwner = isOwner || knownOwnedByViewer || createdByViewerThisSession;
// Once `/collab/status` has EXPLICITLY confirmed this project belongs to
// someone else (shared && !isOwner), remember it for as long as this
// component stays mounted on this projectId. The owner unsharing later
Expand Down Expand Up @@ -332,6 +362,18 @@ export function useProjectCollab(
? false
: (statusUnknown && !knownUnshared) || (shared && !isOwner) || lostAccessAfterUnshare;
const viewerOnly = workspaceContextReadOnly || workspaceReadOnly || sharedReadOnly;
// Positive non-owner evidence only — sticky UX (default-collapsed chat) must
// not latch on `shared && !isOwner` while ownerMemberId is still missing from
// an otherwise-shared status payload for the real owner.
const statusNamedDifferentOwner =
collab.ownerMemberId != null
&& context?.workspaceMemberId != null
&& collab.ownerMemberId !== context.workspaceMemberId;
const isSharedNonOwner =
!isEffectiveOwner
&& (knownOwnedBySomeoneElse
|| (shared && statusNamedDifferentOwner)
|| lostAccessAfterUnshare);

// Member content auto-sync (the last link): when a read-only member sees the
// resource-hub head (`publishedVersion`) advance past what we last pulled,
Expand Down Expand Up @@ -461,6 +503,8 @@ export function useProjectCollab(
syncState: collab.syncState,
viewerOnly,
isOwner,
isEffectiveOwner,
isSharedNonOwner,
ownerDisplayName: collab.ownerDisplayName,
ownerRole: collab.ownerRole,
downloadPending,
Expand Down
33 changes: 33 additions & 0 deletions apps/web/src/components/ProjectView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,20 @@ export function projectSplitClassName(workspaceFocused: boolean): string {
return workspaceFocused ? 'split split-focus' : 'split';
}

/**
* Whether a project open should start with the chat pane collapsed (workspace
* focus mode). Uses `useProjectCollab`'s confirmed shared-non-owner signal
* (`isSharedNonOwner`) — not raw `isOwner` — so a catalog-confirmed owner whose
* `/collab/status` payload is still missing `ownerMemberId` does not latch into
* focus mode permanently (review: sticky apply ref).
*/
export function shouldDefaultCollapseChatForSharedNonOwner(collab: {
enabled: boolean;
isSharedNonOwner: boolean;
}): boolean {
return collab.enabled && collab.isSharedNonOwner;
}

// React key for the on-screen question form. Deliberately does NOT include the
// form's parsed `id`: there is at most one (first) form per assistant message,
// so `${conversation}:${message}` is already a stable, unique identity for the
Expand Down Expand Up @@ -2282,10 +2296,29 @@ export function ProjectView({
setActiveConversationId(routeConversationId);
}, [routeConversationId, conversations, activeConversationId]);

// Reset chat pane to the open default on project switch. Shared non-owner
// projects re-collapse once collab status confirms (see below) — but only
// once per open, so expanding chat after that is sticky for the visit.
const sharedNonOwnerChatDefaultAppliedRef = useRef<string | null>(null);
useEffect(() => {
setWorkspaceFocused(false);
sharedNonOwnerChatDefaultAppliedRef.current = null;
}, [project.id]);

useEffect(() => {
if (sharedNonOwnerChatDefaultAppliedRef.current === project.id) return;
if (
!shouldDefaultCollapseChatForSharedNonOwner({
enabled: projectCollab.enabled,
isSharedNonOwner: projectCollab.isSharedNonOwner,
})
) {
return;
}
setWorkspaceFocused(true);
sharedNonOwnerChatDefaultAppliedRef.current = project.id;
}, [project.id, projectCollab.enabled, projectCollab.isSharedNonOwner]);

// Load messages whenever the active conversation changes. This happens
// on project mount (after conversations load) and on user-triggered
// conversation switches.
Expand Down
12 changes: 6 additions & 6 deletions apps/web/src/i18n/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const ar: Dict = {
"workspaceInvite.removeRow": "Remove",
"workspaceInvite.addMember": "Add member",
"workspaceInvite.visibilityQuestion": "Will team members see my designs?",
"workspaceInvite.visibilityAnswer": "Team members can see designs you share to the team space. Private designs kept in Drafts are not visible to others.",
"workspaceInvite.visibilityAnswer": "Team members can see designs you share to the team space. Private designs kept in Personal projects are not visible to others.",
"workspaceInvite.sent": "Invitation sent",
"workspaceInvite.sending": "Inviting…",
"workspaceInvite.confirm": "Confirm and invite",
Expand Down Expand Up @@ -757,8 +757,8 @@ export const ar: Dict = {
'workspaceSwitcher.team': 'Team',
'workspaceSwitcher.invite': 'Invite colleague',
'workspaceSwitcher.createTeam': 'New team',
"workspaceSwitcher.draftsTooltip": "Drafts",
"workspaceSwitcher.allProjectsTooltip": "All projects",
"workspaceSwitcher.draftsTooltip": "المشاريع الشخصية",
"workspaceSwitcher.allProjectsTooltip": "مشاريع الفريق",
"entry.primaryNavAria": "Primary",
"entry.billingTierTeam": "Teams",
"entry.billingTierFree": "Free",
Expand Down Expand Up @@ -842,8 +842,8 @@ export const ar: Dict = {
'entry.navIntegrations': 'عمليات الدمج',
'entry.navMembers': 'Members',
'entry.navWorkspaceSettings': 'Workspace settings',
'entry.navDrafts': 'Drafts',
'entry.navAllProjects': 'All projects',
'entry.navDrafts': 'المشاريع الشخصية',
'entry.navAllProjects': 'مشاريع الفريق',
'entry.draftsDescription': 'Projects you created, visible only to you',
'entry.allProjectsDescription': 'Projects owned by everyone on the team',
'entry.navBoard': 'Board',
Expand Down Expand Up @@ -1104,7 +1104,7 @@ export const ar: Dict = {
'recentProjects.moveToTeam': 'Move to team space',
'recentProjects.moveToTeamDescPre': 'Once moved to the team space, ',
'recentProjects.moveToTeamDescStrong': 'all team members can view and comment',
'recentProjects.moveToTeamDescPost': ', and only the creator can edit. You can find it under "All projects".',
'recentProjects.moveToTeamDescPost': ', and only the creator can edit. You can find it under "Team projects".',
'recentProjects.moveToPersonalDescPre': 'Once moved out of the team space, it returns to a personal project, ',
'recentProjects.moveToPersonalDescStrong': 'only you can view and edit it',
'recentProjects.moveToPersonalDescPost': '.',
Expand Down
12 changes: 6 additions & 6 deletions apps/web/src/i18n/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const de: Dict = {
"workspaceInvite.removeRow": "Remove",
"workspaceInvite.addMember": "Add member",
"workspaceInvite.visibilityQuestion": "Will team members see my designs?",
"workspaceInvite.visibilityAnswer": "Team members can see designs you share to the team space. Private designs kept in Drafts are not visible to others.",
"workspaceInvite.visibilityAnswer": "Team members can see designs you share to the team space. Private designs kept in Personal projects are not visible to others.",
"workspaceInvite.sent": "Invitation sent",
"workspaceInvite.sending": "Inviting…",
"workspaceInvite.confirm": "Confirm and invite",
Expand Down Expand Up @@ -757,8 +757,8 @@ export const de: Dict = {
'workspaceSwitcher.team': 'Team',
'workspaceSwitcher.invite': 'Invite colleague',
'workspaceSwitcher.createTeam': 'New team',
"workspaceSwitcher.draftsTooltip": "Drafts",
"workspaceSwitcher.allProjectsTooltip": "All projects",
"workspaceSwitcher.draftsTooltip": "Persönliche Projekte",
"workspaceSwitcher.allProjectsTooltip": "Teamprojekte",
"entry.primaryNavAria": "Primary",
"entry.billingTierTeam": "Teams",
"entry.billingTierFree": "Free",
Expand Down Expand Up @@ -842,8 +842,8 @@ export const de: Dict = {
'entry.navIntegrations': 'Integrationen',
'entry.navMembers': 'Members',
'entry.navWorkspaceSettings': 'Workspace settings',
'entry.navDrafts': 'Drafts',
'entry.navAllProjects': 'All projects',
'entry.navDrafts': 'Persönliche Projekte',
'entry.navAllProjects': 'Teamprojekte',
'entry.draftsDescription': 'Projects you created, visible only to you',
'entry.allProjectsDescription': 'Projects owned by everyone on the team',
'entry.navBoard': 'Board',
Expand Down Expand Up @@ -1104,7 +1104,7 @@ export const de: Dict = {
'recentProjects.moveToTeam': 'Move to team space',
'recentProjects.moveToTeamDescPre': 'Once moved to the team space, ',
'recentProjects.moveToTeamDescStrong': 'all team members can view and comment',
'recentProjects.moveToTeamDescPost': ', and only the creator can edit. You can find it under "All projects".',
'recentProjects.moveToTeamDescPost': ', and only the creator can edit. You can find it under "Team projects".',
'recentProjects.moveToPersonalDescPre': 'Once moved out of the team space, it returns to a personal project, ',
'recentProjects.moveToPersonalDescStrong': 'only you can view and edit it',
'recentProjects.moveToPersonalDescPost': '.',
Expand Down
12 changes: 6 additions & 6 deletions apps/web/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const en: Dict = {
"workspaceInvite.removeRow": "Remove",
"workspaceInvite.addMember": "Add member",
"workspaceInvite.visibilityQuestion": "Will team members see my designs?",
"workspaceInvite.visibilityAnswer": "Team members can see designs you share to the team space. Private designs kept in Drafts are not visible to others.",
"workspaceInvite.visibilityAnswer": "Team members can see designs you share to the team space. Private designs kept in Personal projects are not visible to others.",
"workspaceInvite.sent": "Invitation sent",
"workspaceInvite.sending": "Inviting…",
"workspaceInvite.confirm": "Confirm and invite",
Expand Down Expand Up @@ -757,8 +757,8 @@ export const en: Dict = {
'workspaceSwitcher.team': 'Team',
'workspaceSwitcher.invite': 'Invite colleague',
'workspaceSwitcher.createTeam': 'New team',
"workspaceSwitcher.draftsTooltip": "Drafts",
"workspaceSwitcher.allProjectsTooltip": "All projects",
"workspaceSwitcher.draftsTooltip": "Personal projects",
"workspaceSwitcher.allProjectsTooltip": "Team projects",
"entry.primaryNavAria": "Primary",
"entry.billingTierTeam": "Teams",
"entry.billingTierFree": "Free",
Expand Down Expand Up @@ -842,8 +842,8 @@ export const en: Dict = {
'entry.navIntegrations': 'Integrations',
'entry.navMembers': 'Members',
'entry.navWorkspaceSettings': 'Workspace settings',
'entry.navDrafts': 'Drafts',
'entry.navAllProjects': 'All projects',
'entry.navDrafts': 'Personal projects',
'entry.navAllProjects': 'Team projects',
'entry.draftsDescription': 'Projects you created, visible only to you',
'entry.allProjectsDescription': 'Projects owned by everyone on the team',
'entry.navBoard': 'Board',
Expand Down Expand Up @@ -1104,7 +1104,7 @@ export const en: Dict = {
'recentProjects.moveToTeam': 'Move to team space',
'recentProjects.moveToTeamDescPre': 'Once moved to the team space, ',
'recentProjects.moveToTeamDescStrong': 'all team members can view and comment',
'recentProjects.moveToTeamDescPost': ', and only the creator can edit. You can find it under "All projects".',
'recentProjects.moveToTeamDescPost': ', and only the creator can edit. You can find it under "Team projects".',
'recentProjects.moveToPersonalDescPre': 'Once moved out of the team space, it returns to a personal project, ',
'recentProjects.moveToPersonalDescStrong': 'only you can view and edit it',
'recentProjects.moveToPersonalDescPost': '.',
Expand Down
12 changes: 6 additions & 6 deletions apps/web/src/i18n/locales/es-ES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const esES: Dict = {
"workspaceInvite.removeRow": "Remove",
"workspaceInvite.addMember": "Add member",
"workspaceInvite.visibilityQuestion": "Will team members see my designs?",
"workspaceInvite.visibilityAnswer": "Team members can see designs you share to the team space. Private designs kept in Drafts are not visible to others.",
"workspaceInvite.visibilityAnswer": "Team members can see designs you share to the team space. Private designs kept in Personal projects are not visible to others.",
"workspaceInvite.sent": "Invitation sent",
"workspaceInvite.sending": "Inviting…",
"workspaceInvite.confirm": "Confirm and invite",
Expand Down Expand Up @@ -757,8 +757,8 @@ export const esES: Dict = {
'workspaceSwitcher.team': 'Team',
'workspaceSwitcher.invite': 'Invite colleague',
'workspaceSwitcher.createTeam': 'New team',
"workspaceSwitcher.draftsTooltip": "Drafts",
"workspaceSwitcher.allProjectsTooltip": "All projects",
"workspaceSwitcher.draftsTooltip": "Proyectos personales",
"workspaceSwitcher.allProjectsTooltip": "Proyectos del equipo",
"entry.primaryNavAria": "Primary",
"entry.billingTierTeam": "Teams",
"entry.billingTierFree": "Free",
Expand Down Expand Up @@ -842,8 +842,8 @@ export const esES: Dict = {
'entry.navIntegrations': 'Integraciones',
'entry.navMembers': 'Members',
'entry.navWorkspaceSettings': 'Workspace settings',
'entry.navDrafts': 'Drafts',
'entry.navAllProjects': 'All projects',
'entry.navDrafts': 'Proyectos personales',
'entry.navAllProjects': 'Proyectos del equipo',
'entry.draftsDescription': 'Projects you created, visible only to you',
'entry.allProjectsDescription': 'Projects owned by everyone on the team',
'entry.navBoard': 'Board',
Expand Down Expand Up @@ -1104,7 +1104,7 @@ export const esES: Dict = {
'recentProjects.moveToTeam': 'Move to team space',
'recentProjects.moveToTeamDescPre': 'Once moved to the team space, ',
'recentProjects.moveToTeamDescStrong': 'all team members can view and comment',
'recentProjects.moveToTeamDescPost': ', and only the creator can edit. You can find it under "All projects".',
'recentProjects.moveToTeamDescPost': ', and only the creator can edit. You can find it under "Team projects".',
'recentProjects.moveToPersonalDescPre': 'Once moved out of the team space, it returns to a personal project, ',
'recentProjects.moveToPersonalDescStrong': 'only you can view and edit it',
'recentProjects.moveToPersonalDescPost': '.',
Expand Down
12 changes: 6 additions & 6 deletions apps/web/src/i18n/locales/fa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const fa: Dict = {
"workspaceInvite.removeRow": "Remove",
"workspaceInvite.addMember": "Add member",
"workspaceInvite.visibilityQuestion": "Will team members see my designs?",
"workspaceInvite.visibilityAnswer": "Team members can see designs you share to the team space. Private designs kept in Drafts are not visible to others.",
"workspaceInvite.visibilityAnswer": "Team members can see designs you share to the team space. Private designs kept in Personal projects are not visible to others.",
"workspaceInvite.sent": "Invitation sent",
"workspaceInvite.sending": "Inviting…",
"workspaceInvite.confirm": "Confirm and invite",
Expand Down Expand Up @@ -757,8 +757,8 @@ export const fa: Dict = {
'workspaceSwitcher.team': 'Team',
'workspaceSwitcher.invite': 'Invite colleague',
'workspaceSwitcher.createTeam': 'New team',
"workspaceSwitcher.draftsTooltip": "Drafts",
"workspaceSwitcher.allProjectsTooltip": "All projects",
"workspaceSwitcher.draftsTooltip": "پروژه‌های شخصی",
"workspaceSwitcher.allProjectsTooltip": "پروژه‌های تیم",
"entry.primaryNavAria": "Primary",
"entry.billingTierTeam": "Teams",
"entry.billingTierFree": "Free",
Expand Down Expand Up @@ -842,8 +842,8 @@ export const fa: Dict = {
'entry.navIntegrations': 'یکپارچه‌سازی‌ها',
'entry.navMembers': 'Members',
'entry.navWorkspaceSettings': 'Workspace settings',
'entry.navDrafts': 'Drafts',
'entry.navAllProjects': 'All projects',
'entry.navDrafts': 'پروژه‌های شخصی',
'entry.navAllProjects': 'پروژه‌های تیم',
'entry.draftsDescription': 'Projects you created, visible only to you',
'entry.allProjectsDescription': 'Projects owned by everyone on the team',
'entry.navBoard': 'Board',
Expand Down Expand Up @@ -1104,7 +1104,7 @@ export const fa: Dict = {
'recentProjects.moveToTeam': 'Move to team space',
'recentProjects.moveToTeamDescPre': 'Once moved to the team space, ',
'recentProjects.moveToTeamDescStrong': 'all team members can view and comment',
'recentProjects.moveToTeamDescPost': ', and only the creator can edit. You can find it under "All projects".',
'recentProjects.moveToTeamDescPost': ', and only the creator can edit. You can find it under "Team projects".',
'recentProjects.moveToPersonalDescPre': 'Once moved out of the team space, it returns to a personal project, ',
'recentProjects.moveToPersonalDescStrong': 'only you can view and edit it',
'recentProjects.moveToPersonalDescPost': '.',
Expand Down
Loading
Loading