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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@
номера версий по [семантике](https://semver.org/lang/ru/), то есть до `1.0.0` интерфейс команд
ещё может меняться.

## Не выпущено

### Исправлено

- **Чат, в который владелец вернулся, больше не остаётся помеченным «покинут»** (cli-core 0.352.0). Неполный
список чатов и вход снимают эту метку с чатов, которые они называют; «в архиве» и другие состояния остаются.
Раньше такой чат мог пропасть из `max chats list`, а `store clear --left` удалял его вместе с сообщениями.
Полный список, как и прежде, помечает «покинут» чаты, которых в нём нет.

## 0.64.0 — 19.10.2026

### Что нового
Expand Down
3 changes: 2 additions & 1 deletion docs/dev/architecture/store.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ with `max messages sync` to make older messages available.
## Departed chats

Only a complete nonempty chat snapshot marks missing chats as left. Cut and empty snapshots keep
previous chats. `max store clear --left` previews the data; `--allow-dangerous` removes it.
previous chats. Every snapshot, cut or complete, and every login takes the left mark off the chats it
names; other states such as archived stay. `max store clear --left` previews the data; `--allow-dangerous` removes it.
There is no whole-store wipe command. `max doctor` names any remaining legacy profile file;
its data is not migrated into the shared store.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
"@msgpack/msgpack": "3.1.3",
"@valibot/to-json-schema": "1.8.0",
"@wirecat/cli-base": "0.47.0",
"@wirecat/cli-core": "0.350.0",
"@wirecat/cli-core": "0.352.0",
"commander": "15.0.0",
"lossless-json": "4.3.1",
"ogg-opus-decoder": "1.7.5",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 71 additions & 0 deletions src/record.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,75 @@ describe("the record", () => {
expect(await store.sync.syncState(key, "login.marker")).toBeUndefined()
await store.close()
})

describe("rejoining chats the owner is back in", () => {
const key = { provider: "max", account: ME }
const other = { provider: "max", account: "10000009" }
const chat = (id: string) => ({
id,
title: `Chat ${id}`,
kind: "group" as const,
unreadCount: 0,
lastMessageAt: null,
participantsCount: null,
})
const seeded = async () => {
const { env } = setUp()
const store = await openStore({ env })
await store.chats.saveChats(key, [
{ ...chat("111"), membershipState: "left" },
{ ...chat("222"), membershipState: "archived" },
chat("333"),
])
await store.chats.saveChats(other, [{ ...chat("111"), membershipState: "left" }])
await store.close()
return { env, record: maxRecord({ account: () => ME, env }) }
}
const states = async (env: { MESSAGING_STORE: string }) => {
const store = await openStore({ env })
const left = await store.chats.leftChats(key)
const all = (await store.chats.chats(key, {})).items
const otherLeft = await store.chats.leftChats(other)
await store.close()
return {
left: left.chats,
otherLeft: otherLeft.chats,
listed: all.map((stored) => [stored.id, stored.membershipState]),
}
}

it("a cut-short list takes the left mark off a chat it names and keeps the archived one", async () => {
const { env, record } = await seeded()
const known = await record.rememberChats([chat("111"), chat("222")], false)
await record.close()

expect(known.map((stored) => stored.id).sort()).toEqual(["111", "222", "333"])
expect(await states(env)).toMatchObject({
left: 0,
otherLeft: 1,
listed: expect.arrayContaining([["222", "archived"]]),
})
})

it("a complete list still marks the chats it leaves out as left", async () => {
const { env, record } = await seeded()
const known = await record.rememberChats([chat("111"), chat("222")], true)
await record.close()

expect(known.map((stored) => stored.id).sort()).toEqual(["111", "222"])
expect(await states(env)).toMatchObject({ left: 1, listed: expect.arrayContaining([["222", "archived"]]) })
})

it("a login takes the left mark off the chats it names and keeps the archived one", async () => {
const { env, record } = await seeded()
await record.applyLogin({ chats: [chat("111"), chat("222")], people: [], members: new Map() })
await record.close()

expect(await states(env)).toMatchObject({
left: 0,
otherLeft: 1,
listed: expect.arrayContaining([["222", "archived"]]),
})
})
})
})
13 changes: 8 additions & 5 deletions src/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ export const maxRecord = ({ account, env }: { account: () => Id | undefined; env
members,
...(marker === undefined ? {} : { state: { [MARKER]: String(marker) } }),
})
if (chats.length > 0)
await db.chats.rejoinChats(
key,
chats.map((chat) => chat.id),
)
return {
known: (await db.people.people("max", { account: key.account })).all().length,
added,
Expand All @@ -103,11 +108,9 @@ export const maxRecord = ({ account, env }: { account: () => Id | undefined; env
if (!key) return chats
const db = await store()
await db.chats.saveChats(key, chats)
if (complete && chats.length > 0)
await db.chats.markChatsLeft(
key,
chats.map((chat) => chat.id),
)
const ids = chats.map((chat) => chat.id)
if (complete && ids.length > 0) await db.chats.markChatsLeft(key, ids)
else if (ids.length > 0) await db.chats.rejoinChats(key, ids)
return (await db.chats.chats(key, {})).items
},

Expand Down
Loading