Skip to content

Commit 6801969

Browse files
authored
feat(web): personal/team project labels + collapse chat for shared non-owners (#6294)
* feat(web): rename sidebar project lists and collapse chat for shared non-owners Workspace home left rail now labels personal vs team project lists ("Personal projects" / "Team projects", 个人项目 / 团队项目) across all locales. Opening a confirmed shared team project that the viewer did not create defaults the chat pane to collapsed so the design is primary; owners and personal projects keep chat open, and expanding chat stays sticky for the visit. * fix(web): use effective ownership for shared non-owner chat default Default-collapsed chat was latching on raw isOwner, so a catalog-confirmed owner whose collab status was still missing ownerMemberId looked like a non-owner and permanently collapsed chat. Expose isEffectiveOwner and isSharedNonOwner from useProjectCollab (catalog + session-created + status) and gate the sticky collapse path on the confirmed non-owner signal only.
1 parent a5715da commit 6801969

27 files changed

Lines changed: 285 additions & 113 deletions

apps/web/src/collab/collab-context.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ const DISABLED: CollabContextValue = {
2020
syncState: null,
2121
viewerOnly: false,
2222
isOwner: false,
23+
isEffectiveOwner: false,
24+
isSharedNonOwner: false,
2325
ownerDisplayName: null,
2426
ownerRole: null,
2527
downloadPending: false,

apps/web/src/collab/useProjectCollab.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,22 @@ export interface ProjectCollab {
175175
* re-deriving ownership at the call site.
176176
*/
177177
isOwner: boolean;
178+
/**
179+
* Effective project ownership for UX that must not wait on `/collab/status`.
180+
* True when status confirms the viewer is the single writer **or** the hub
181+
* catalog already names them as owner (issue #99) **or** this tab created
182+
* the project this session. Prefer this over raw `isOwner` for sticky defaults
183+
* (e.g. default chat open/collapsed) that would otherwise latch wrong while
184+
* `ownerMemberId` is still missing from a shared status payload.
185+
*/
186+
isEffectiveOwner: boolean;
187+
/**
188+
* Confirmed: this is a shared team project the viewer did **not** create.
189+
* Requires positive evidence of a different owner (status `ownerMemberId`,
190+
* catalog owner, or a previously confirmed non-owner relationship) — never
191+
* mere `!isOwner` during the status-unknown window.
192+
*/
193+
isSharedNonOwner: boolean;
178194
/**
179195
* Display name of the member who shared this project (its owner), resolved
180196
* server-side from the collab-cloud member directory. Drives the "这是 {owner}
@@ -296,13 +312,27 @@ export function useProjectCollab(
296312
&& knownCatalog.some(
297313
(entry) => entry.projectId === projectId && entry.ownerMemberId === context.workspaceMemberId,
298314
);
315+
// Catalog names a different member as the single writer — usable before
316+
// `/collab/status` confirms ownerMemberId (mirror of knownOwnedByViewer).
317+
const knownOwnedBySomeoneElse =
318+
knownCatalog !== null
319+
&& Boolean(projectId)
320+
&& context?.workspaceMemberId != null
321+
&& knownCatalog.some(
322+
(entry) =>
323+
entry.projectId === projectId
324+
&& entry.ownerMemberId != null
325+
&& entry.ownerMemberId !== context.workspaceMemberId,
326+
);
299327
// A project this browser tab created moments ago cannot possibly be shared
300328
// yet — see `projectIdsCreatedByViewerThisSession` above. This covers the
301329
// window `knownUnshared` cannot: right after creation the project is not
302330
// (and cannot yet be) in the team catalog at all, so `knownCatalog` loading
303331
// or not loading is irrelevant here.
304332
const createdByViewerThisSession =
305333
Boolean(projectId) && projectIdsCreatedByViewerThisSession.has(projectId as string);
334+
// Status-confirmed ownership OR catalog/session shortcuts (issue #99 path).
335+
const isEffectiveOwner = isOwner || knownOwnedByViewer || createdByViewerThisSession;
306336
// Once `/collab/status` has EXPLICITLY confirmed this project belongs to
307337
// someone else (shared && !isOwner), remember it for as long as this
308338
// component stays mounted on this projectId. The owner unsharing later
@@ -332,6 +362,18 @@ export function useProjectCollab(
332362
? false
333363
: (statusUnknown && !knownUnshared) || (shared && !isOwner) || lostAccessAfterUnshare;
334364
const viewerOnly = workspaceContextReadOnly || workspaceReadOnly || sharedReadOnly;
365+
// Positive non-owner evidence only — sticky UX (default-collapsed chat) must
366+
// not latch on `shared && !isOwner` while ownerMemberId is still missing from
367+
// an otherwise-shared status payload for the real owner.
368+
const statusNamedDifferentOwner =
369+
collab.ownerMemberId != null
370+
&& context?.workspaceMemberId != null
371+
&& collab.ownerMemberId !== context.workspaceMemberId;
372+
const isSharedNonOwner =
373+
!isEffectiveOwner
374+
&& (knownOwnedBySomeoneElse
375+
|| (shared && statusNamedDifferentOwner)
376+
|| lostAccessAfterUnshare);
335377

336378
// Member content auto-sync (the last link): when a read-only member sees the
337379
// resource-hub head (`publishedVersion`) advance past what we last pulled,
@@ -461,6 +503,8 @@ export function useProjectCollab(
461503
syncState: collab.syncState,
462504
viewerOnly,
463505
isOwner,
506+
isEffectiveOwner,
507+
isSharedNonOwner,
464508
ownerDisplayName: collab.ownerDisplayName,
465509
ownerRole: collab.ownerRole,
466510
downloadPending,

apps/web/src/components/ProjectView.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,6 +1186,20 @@ export function projectSplitClassName(workspaceFocused: boolean): string {
11861186
return workspaceFocused ? 'split split-focus' : 'split';
11871187
}
11881188

1189+
/**
1190+
* Whether a project open should start with the chat pane collapsed (workspace
1191+
* focus mode). Uses `useProjectCollab`'s confirmed shared-non-owner signal
1192+
* (`isSharedNonOwner`) — not raw `isOwner` — so a catalog-confirmed owner whose
1193+
* `/collab/status` payload is still missing `ownerMemberId` does not latch into
1194+
* focus mode permanently (review: sticky apply ref).
1195+
*/
1196+
export function shouldDefaultCollapseChatForSharedNonOwner(collab: {
1197+
enabled: boolean;
1198+
isSharedNonOwner: boolean;
1199+
}): boolean {
1200+
return collab.enabled && collab.isSharedNonOwner;
1201+
}
1202+
11891203
// React key for the on-screen question form. Deliberately does NOT include the
11901204
// form's parsed `id`: there is at most one (first) form per assistant message,
11911205
// so `${conversation}:${message}` is already a stable, unique identity for the
@@ -2282,10 +2296,29 @@ export function ProjectView({
22822296
setActiveConversationId(routeConversationId);
22832297
}, [routeConversationId, conversations, activeConversationId]);
22842298

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

2308+
useEffect(() => {
2309+
if (sharedNonOwnerChatDefaultAppliedRef.current === project.id) return;
2310+
if (
2311+
!shouldDefaultCollapseChatForSharedNonOwner({
2312+
enabled: projectCollab.enabled,
2313+
isSharedNonOwner: projectCollab.isSharedNonOwner,
2314+
})
2315+
) {
2316+
return;
2317+
}
2318+
setWorkspaceFocused(true);
2319+
sharedNonOwnerChatDefaultAppliedRef.current = project.id;
2320+
}, [project.id, projectCollab.enabled, projectCollab.isSharedNonOwner]);
2321+
22892322
// Load messages whenever the active conversation changes. This happens
22902323
// on project mount (after conversations load) and on user-triggered
22912324
// conversation switches.

apps/web/src/i18n/locales/ar.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const ar: Dict = {
5252
"workspaceInvite.removeRow": "Remove",
5353
"workspaceInvite.addMember": "Add member",
5454
"workspaceInvite.visibilityQuestion": "Will team members see my designs?",
55-
"workspaceInvite.visibilityAnswer": "Team members can see designs you share to the team space. Private designs kept in Drafts are not visible to others.",
55+
"workspaceInvite.visibilityAnswer": "Team members can see designs you share to the team space. Private designs kept in Personal projects are not visible to others.",
5656
"workspaceInvite.sent": "Invitation sent",
5757
"workspaceInvite.sending": "Inviting…",
5858
"workspaceInvite.confirm": "Confirm and invite",
@@ -757,8 +757,8 @@ export const ar: Dict = {
757757
'workspaceSwitcher.team': 'Team',
758758
'workspaceSwitcher.invite': 'Invite colleague',
759759
'workspaceSwitcher.createTeam': 'New team',
760-
"workspaceSwitcher.draftsTooltip": "Drafts",
761-
"workspaceSwitcher.allProjectsTooltip": "All projects",
760+
"workspaceSwitcher.draftsTooltip": "المشاريع الشخصية",
761+
"workspaceSwitcher.allProjectsTooltip": "مشاريع الفريق",
762762
"entry.primaryNavAria": "Primary",
763763
"entry.billingTierTeam": "Teams",
764764
"entry.billingTierFree": "Free",
@@ -842,8 +842,8 @@ export const ar: Dict = {
842842
'entry.navIntegrations': 'عمليات الدمج',
843843
'entry.navMembers': 'Members',
844844
'entry.navWorkspaceSettings': 'Workspace settings',
845-
'entry.navDrafts': 'Drafts',
846-
'entry.navAllProjects': 'All projects',
845+
'entry.navDrafts': 'المشاريع الشخصية',
846+
'entry.navAllProjects': 'مشاريع الفريق',
847847
'entry.draftsDescription': 'Projects you created, visible only to you',
848848
'entry.allProjectsDescription': 'Projects owned by everyone on the team',
849849
'entry.navBoard': 'Board',
@@ -1104,7 +1104,7 @@ export const ar: Dict = {
11041104
'recentProjects.moveToTeam': 'Move to team space',
11051105
'recentProjects.moveToTeamDescPre': 'Once moved to the team space, ',
11061106
'recentProjects.moveToTeamDescStrong': 'all team members can view and comment',
1107-
'recentProjects.moveToTeamDescPost': ', and only the creator can edit. You can find it under "All projects".',
1107+
'recentProjects.moveToTeamDescPost': ', and only the creator can edit. You can find it under "Team projects".',
11081108
'recentProjects.moveToPersonalDescPre': 'Once moved out of the team space, it returns to a personal project, ',
11091109
'recentProjects.moveToPersonalDescStrong': 'only you can view and edit it',
11101110
'recentProjects.moveToPersonalDescPost': '.',

apps/web/src/i18n/locales/de.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const de: Dict = {
5252
"workspaceInvite.removeRow": "Remove",
5353
"workspaceInvite.addMember": "Add member",
5454
"workspaceInvite.visibilityQuestion": "Will team members see my designs?",
55-
"workspaceInvite.visibilityAnswer": "Team members can see designs you share to the team space. Private designs kept in Drafts are not visible to others.",
55+
"workspaceInvite.visibilityAnswer": "Team members can see designs you share to the team space. Private designs kept in Personal projects are not visible to others.",
5656
"workspaceInvite.sent": "Invitation sent",
5757
"workspaceInvite.sending": "Inviting…",
5858
"workspaceInvite.confirm": "Confirm and invite",
@@ -757,8 +757,8 @@ export const de: Dict = {
757757
'workspaceSwitcher.team': 'Team',
758758
'workspaceSwitcher.invite': 'Invite colleague',
759759
'workspaceSwitcher.createTeam': 'New team',
760-
"workspaceSwitcher.draftsTooltip": "Drafts",
761-
"workspaceSwitcher.allProjectsTooltip": "All projects",
760+
"workspaceSwitcher.draftsTooltip": "Persönliche Projekte",
761+
"workspaceSwitcher.allProjectsTooltip": "Teamprojekte",
762762
"entry.primaryNavAria": "Primary",
763763
"entry.billingTierTeam": "Teams",
764764
"entry.billingTierFree": "Free",
@@ -842,8 +842,8 @@ export const de: Dict = {
842842
'entry.navIntegrations': 'Integrationen',
843843
'entry.navMembers': 'Members',
844844
'entry.navWorkspaceSettings': 'Workspace settings',
845-
'entry.navDrafts': 'Drafts',
846-
'entry.navAllProjects': 'All projects',
845+
'entry.navDrafts': 'Persönliche Projekte',
846+
'entry.navAllProjects': 'Teamprojekte',
847847
'entry.draftsDescription': 'Projects you created, visible only to you',
848848
'entry.allProjectsDescription': 'Projects owned by everyone on the team',
849849
'entry.navBoard': 'Board',
@@ -1104,7 +1104,7 @@ export const de: Dict = {
11041104
'recentProjects.moveToTeam': 'Move to team space',
11051105
'recentProjects.moveToTeamDescPre': 'Once moved to the team space, ',
11061106
'recentProjects.moveToTeamDescStrong': 'all team members can view and comment',
1107-
'recentProjects.moveToTeamDescPost': ', and only the creator can edit. You can find it under "All projects".',
1107+
'recentProjects.moveToTeamDescPost': ', and only the creator can edit. You can find it under "Team projects".',
11081108
'recentProjects.moveToPersonalDescPre': 'Once moved out of the team space, it returns to a personal project, ',
11091109
'recentProjects.moveToPersonalDescStrong': 'only you can view and edit it',
11101110
'recentProjects.moveToPersonalDescPost': '.',

apps/web/src/i18n/locales/en.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const en: Dict = {
5252
"workspaceInvite.removeRow": "Remove",
5353
"workspaceInvite.addMember": "Add member",
5454
"workspaceInvite.visibilityQuestion": "Will team members see my designs?",
55-
"workspaceInvite.visibilityAnswer": "Team members can see designs you share to the team space. Private designs kept in Drafts are not visible to others.",
55+
"workspaceInvite.visibilityAnswer": "Team members can see designs you share to the team space. Private designs kept in Personal projects are not visible to others.",
5656
"workspaceInvite.sent": "Invitation sent",
5757
"workspaceInvite.sending": "Inviting…",
5858
"workspaceInvite.confirm": "Confirm and invite",
@@ -757,8 +757,8 @@ export const en: Dict = {
757757
'workspaceSwitcher.team': 'Team',
758758
'workspaceSwitcher.invite': 'Invite colleague',
759759
'workspaceSwitcher.createTeam': 'New team',
760-
"workspaceSwitcher.draftsTooltip": "Drafts",
761-
"workspaceSwitcher.allProjectsTooltip": "All projects",
760+
"workspaceSwitcher.draftsTooltip": "Personal projects",
761+
"workspaceSwitcher.allProjectsTooltip": "Team projects",
762762
"entry.primaryNavAria": "Primary",
763763
"entry.billingTierTeam": "Teams",
764764
"entry.billingTierFree": "Free",
@@ -842,8 +842,8 @@ export const en: Dict = {
842842
'entry.navIntegrations': 'Integrations',
843843
'entry.navMembers': 'Members',
844844
'entry.navWorkspaceSettings': 'Workspace settings',
845-
'entry.navDrafts': 'Drafts',
846-
'entry.navAllProjects': 'All projects',
845+
'entry.navDrafts': 'Personal projects',
846+
'entry.navAllProjects': 'Team projects',
847847
'entry.draftsDescription': 'Projects you created, visible only to you',
848848
'entry.allProjectsDescription': 'Projects owned by everyone on the team',
849849
'entry.navBoard': 'Board',
@@ -1104,7 +1104,7 @@ export const en: Dict = {
11041104
'recentProjects.moveToTeam': 'Move to team space',
11051105
'recentProjects.moveToTeamDescPre': 'Once moved to the team space, ',
11061106
'recentProjects.moveToTeamDescStrong': 'all team members can view and comment',
1107-
'recentProjects.moveToTeamDescPost': ', and only the creator can edit. You can find it under "All projects".',
1107+
'recentProjects.moveToTeamDescPost': ', and only the creator can edit. You can find it under "Team projects".',
11081108
'recentProjects.moveToPersonalDescPre': 'Once moved out of the team space, it returns to a personal project, ',
11091109
'recentProjects.moveToPersonalDescStrong': 'only you can view and edit it',
11101110
'recentProjects.moveToPersonalDescPost': '.',

apps/web/src/i18n/locales/es-ES.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const esES: Dict = {
5252
"workspaceInvite.removeRow": "Remove",
5353
"workspaceInvite.addMember": "Add member",
5454
"workspaceInvite.visibilityQuestion": "Will team members see my designs?",
55-
"workspaceInvite.visibilityAnswer": "Team members can see designs you share to the team space. Private designs kept in Drafts are not visible to others.",
55+
"workspaceInvite.visibilityAnswer": "Team members can see designs you share to the team space. Private designs kept in Personal projects are not visible to others.",
5656
"workspaceInvite.sent": "Invitation sent",
5757
"workspaceInvite.sending": "Inviting…",
5858
"workspaceInvite.confirm": "Confirm and invite",
@@ -757,8 +757,8 @@ export const esES: Dict = {
757757
'workspaceSwitcher.team': 'Team',
758758
'workspaceSwitcher.invite': 'Invite colleague',
759759
'workspaceSwitcher.createTeam': 'New team',
760-
"workspaceSwitcher.draftsTooltip": "Drafts",
761-
"workspaceSwitcher.allProjectsTooltip": "All projects",
760+
"workspaceSwitcher.draftsTooltip": "Proyectos personales",
761+
"workspaceSwitcher.allProjectsTooltip": "Proyectos del equipo",
762762
"entry.primaryNavAria": "Primary",
763763
"entry.billingTierTeam": "Teams",
764764
"entry.billingTierFree": "Free",
@@ -842,8 +842,8 @@ export const esES: Dict = {
842842
'entry.navIntegrations': 'Integraciones',
843843
'entry.navMembers': 'Members',
844844
'entry.navWorkspaceSettings': 'Workspace settings',
845-
'entry.navDrafts': 'Drafts',
846-
'entry.navAllProjects': 'All projects',
845+
'entry.navDrafts': 'Proyectos personales',
846+
'entry.navAllProjects': 'Proyectos del equipo',
847847
'entry.draftsDescription': 'Projects you created, visible only to you',
848848
'entry.allProjectsDescription': 'Projects owned by everyone on the team',
849849
'entry.navBoard': 'Board',
@@ -1104,7 +1104,7 @@ export const esES: Dict = {
11041104
'recentProjects.moveToTeam': 'Move to team space',
11051105
'recentProjects.moveToTeamDescPre': 'Once moved to the team space, ',
11061106
'recentProjects.moveToTeamDescStrong': 'all team members can view and comment',
1107-
'recentProjects.moveToTeamDescPost': ', and only the creator can edit. You can find it under "All projects".',
1107+
'recentProjects.moveToTeamDescPost': ', and only the creator can edit. You can find it under "Team projects".',
11081108
'recentProjects.moveToPersonalDescPre': 'Once moved out of the team space, it returns to a personal project, ',
11091109
'recentProjects.moveToPersonalDescStrong': 'only you can view and edit it',
11101110
'recentProjects.moveToPersonalDescPost': '.',

apps/web/src/i18n/locales/fa.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const fa: Dict = {
5252
"workspaceInvite.removeRow": "Remove",
5353
"workspaceInvite.addMember": "Add member",
5454
"workspaceInvite.visibilityQuestion": "Will team members see my designs?",
55-
"workspaceInvite.visibilityAnswer": "Team members can see designs you share to the team space. Private designs kept in Drafts are not visible to others.",
55+
"workspaceInvite.visibilityAnswer": "Team members can see designs you share to the team space. Private designs kept in Personal projects are not visible to others.",
5656
"workspaceInvite.sent": "Invitation sent",
5757
"workspaceInvite.sending": "Inviting…",
5858
"workspaceInvite.confirm": "Confirm and invite",
@@ -757,8 +757,8 @@ export const fa: Dict = {
757757
'workspaceSwitcher.team': 'Team',
758758
'workspaceSwitcher.invite': 'Invite colleague',
759759
'workspaceSwitcher.createTeam': 'New team',
760-
"workspaceSwitcher.draftsTooltip": "Drafts",
761-
"workspaceSwitcher.allProjectsTooltip": "All projects",
760+
"workspaceSwitcher.draftsTooltip": "پروژه‌های شخصی",
761+
"workspaceSwitcher.allProjectsTooltip": "پروژه‌های تیم",
762762
"entry.primaryNavAria": "Primary",
763763
"entry.billingTierTeam": "Teams",
764764
"entry.billingTierFree": "Free",
@@ -842,8 +842,8 @@ export const fa: Dict = {
842842
'entry.navIntegrations': 'یکپارچه‌سازی‌ها',
843843
'entry.navMembers': 'Members',
844844
'entry.navWorkspaceSettings': 'Workspace settings',
845-
'entry.navDrafts': 'Drafts',
846-
'entry.navAllProjects': 'All projects',
845+
'entry.navDrafts': 'پروژه‌های شخصی',
846+
'entry.navAllProjects': 'پروژه‌های تیم',
847847
'entry.draftsDescription': 'Projects you created, visible only to you',
848848
'entry.allProjectsDescription': 'Projects owned by everyone on the team',
849849
'entry.navBoard': 'Board',
@@ -1104,7 +1104,7 @@ export const fa: Dict = {
11041104
'recentProjects.moveToTeam': 'Move to team space',
11051105
'recentProjects.moveToTeamDescPre': 'Once moved to the team space, ',
11061106
'recentProjects.moveToTeamDescStrong': 'all team members can view and comment',
1107-
'recentProjects.moveToTeamDescPost': ', and only the creator can edit. You can find it under "All projects".',
1107+
'recentProjects.moveToTeamDescPost': ', and only the creator can edit. You can find it under "Team projects".',
11081108
'recentProjects.moveToPersonalDescPre': 'Once moved out of the team space, it returns to a personal project, ',
11091109
'recentProjects.moveToPersonalDescStrong': 'only you can view and edit it',
11101110
'recentProjects.moveToPersonalDescPost': '.',

0 commit comments

Comments
 (0)