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
54 changes: 47 additions & 7 deletions packages/client-runtime/src/state/identity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe("filterPeopleForTypeahead", () => {
});

describe("threadMatchesMine", () => {
it("filters mine vs theirs", () => {
it("filters mine vs theirs when threads are person-attributed", () => {
expect(
threadMatchesMine({
claimPersonId: "patroza",
Expand All @@ -87,26 +87,66 @@ describe("threadMatchesMine", () => {
).toBe(true);
});

it("excludes both mine and theirs when there is no claim for the env", () => {
it("treats unattributed threads as mine (legacy / channel-only / identity off)", () => {
// No origin or participants — old threads, { channel: "desktop" } only, etc.
expect(
threadMatchesMine({
claimPersonId: null,
originPersonId: "patroza",
claimPersonId: "patroza",
originPersonId: null,
participantPersonIds: [],
mode: "mine",
}),
).toBe(true);
expect(
threadMatchesMine({
claimPersonId: "patroza",
originPersonId: null,
participantPersonIds: [],
mode: "theirs",
}),
).toBe(false);
// Identity-disabled env: no claim, no person tags → Mine still shows work.
expect(
threadMatchesMine({
claimPersonId: null,
originPersonId: "patroza",
originPersonId: undefined,
participantPersonIds: undefined,
mode: "mine",
}),
).toBe(true);
expect(
threadMatchesMine({
claimPersonId: null,
originPersonId: undefined,
mode: "theirs",
}),
).toBe(false);
});

it("treats attributed threads without a session claim as theirs", () => {
expect(
threadMatchesMine({
claimPersonId: null,
originPersonId: "patroza",
mode: "any",
originPersonId: "julius",
mode: "mine",
}),
).toBe(false);
expect(
threadMatchesMine({
claimPersonId: null,
originPersonId: "julius",
mode: "theirs",
}),
).toBe(true);
});

it("includes participant-only matches as mine", () => {
expect(
threadMatchesMine({
claimPersonId: "patroza",
originPersonId: "julius",
participantPersonIds: ["patroza"],
mode: "mine",
}),
).toBe(true);
});
Expand Down
39 changes: 30 additions & 9 deletions packages/client-runtime/src/state/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,24 +73,45 @@ export function filterPeopleForTypeahead(
});
}

/** Match a thread as "mine" against the session claim personId. */
/**
* Match a thread for Mine / Theirs ownership filters.
*
* **Mine** includes:
* - threads where the session claim person appears on origin or participants
* - threads with **no person attribution** (no identity tags, channel-only
* stamps like `{ channel: "desktop" }`, identity-disabled servers, legacy
* threads) — treated as "ours" so filters stay useful offline of a map
*
* **Theirs** is only threads that have at least one person tag and do not
* include the claim person.
*/
export function threadMatchesMine(input: {
readonly claimPersonId: string | null | undefined;
readonly originPersonId?: string | null | undefined;
readonly participantPersonIds?: ReadonlyArray<string> | null | undefined;
readonly mode: "mine" | "theirs" | "any";
}): boolean {
if (input.mode === "any") return true;
const claimId = input.claimPersonId?.trim().toLowerCase() ?? "";
// No claim for this environment (map off, or user never signed up there):
// ownership is unclassifiable — hide from both Mine and Theirs. Multi-env
// clients with primary=smart (no map) previously used a single empty claim
// and treated every thread as Theirs, which made Mine look broken for t3vm.
if (claimId.length === 0) return false;

const people = new Set<string>();
if (input.originPersonId) people.add(input.originPersonId.trim().toLowerCase());
const origin = input.originPersonId?.trim().toLowerCase() ?? "";
if (origin.length > 0) people.add(origin);
for (const id of input.participantPersonIds ?? []) {
people.add(id.trim().toLowerCase());
const personId = id?.trim().toLowerCase() ?? "";
if (personId.length > 0) people.add(personId);
}
const unattributed = people.size === 0;

// No person tags (channel-only source, identity off, pre-attribution history).
if (unattributed) {
return input.mode === "mine";
}

const claimId = input.claimPersonId?.trim().toLowerCase() ?? "";
// Attributed threads need a claim to classify as mine; without a claim they
// are someone else's tags on a map-enabled env (or another person's work).
if (claimId.length === 0) {
return input.mode === "theirs";
}
const isMine = people.has(claimId);
return input.mode === "mine" ? isMine : !isMine;
Expand Down
Loading