Ask for shareWith, or the server does not send it

Nothing was ever badged as shared, "Stop sharing" never appeared, and the
share dialog opened on "not shared with anyone yet" over live shares. The
sharing itself was fine. The client simply never learned about it.

Stalwart does not return `shareWith` unless a client names it. A
`Calendar/get` or `AddressBook/get` with no `properties` comes back
without the field at all -- not null, not empty, absent -- confirmed
against the live 0.16.19 on a calendar and an address book that really
were shared with another account. Omit the list and there is no
`shareWith`; name it and the sharee is right there.

Both stores fetched everything by asking for nothing, and got less than
they would have by asking. They name the properties now.

The dialog is the part worth dwelling on. It seeds itself from the
`shareWith` it was handed, so it has been showing an empty sharee list on
collections that were shared -- the one screen whose whole job is
managing sharing, and the one most confidently wrong about it. Someone
looking there to see who had access, or to take it away, was told there
was nobody.

Files never had this: `fileNodeProps` has named the property since file
sharing went in, for the same reason and after the same surprise. The two
stores that fetched with `ids: null` and no properties are the two that
were blind.

The mock now omits it the same way. One that hands `shareWith` over
unasked lets a client that never asks look correct everywhere except
against a real server, which is exactly how this got here.

Verified against that mock: sharing a calendar puts the sharee in the
store, badges the row, adds "Stop sharing", and the dialog lists them --
while a `Calendar/get` with no properties still comes back without the
field, so the mock is now failing the way the server does.
This commit is contained in:
2026-08-27 13:25:00 -07:00
parent f83157464c
commit 506865ca67
4 changed files with 60 additions and 6 deletions
+33 -2
View File
@@ -15,6 +15,37 @@ export interface EventInstance {
calendar: Calendar | undefined;
}
/*
* Asked for by name, because `shareWith` is not among the properties Stalwart
* returns by default.
*
* A `Calendar/get` with no `properties` comes back without it -- not null, not
* empty, absent -- confirmed against 0.16.19 on 2026-08-27 with a calendar that
* was genuinely shared: omit the list and there is no `shareWith`; name it and
* the sharee is right there. So the client believed nothing was ever shared.
* The badge never appeared, "Stop sharing" never appeared, and the share dialog
* opened on "not shared with anyone yet" over a live share.
*
* Files had this right already, for the same reason and after the same
* surprise; calendars and address books did not.
*/
export const CALENDAR_PROPS = [
"id",
"name",
"description",
"color",
"sortOrder",
"isSubscribed",
"isVisible",
"isDefault",
"includeInAvailability",
"defaultAlertsWithTime",
"defaultAlertsWithoutTime",
"timeZone",
"shareWith",
"myRights",
];
/** A calendar somebody else shared, and the account it lives in. */
export interface SharedCalendar {
accountId: Id;
@@ -131,7 +162,7 @@ export const useCalendar = create<CalendarState>((set, get) => ({
const found: SharedCalendar[] = [];
for (const [accountId, account] of accounts) {
try {
const res = await client.call<GetResponse<Calendar>>("Calendar/get", { accountId, ids: null });
const res = await client.call<GetResponse<Calendar>>("Calendar/get", { accountId, ids: null, properties: CALENDAR_PROPS });
for (const calendar of res.list) found.push({ accountId, accountName: account.name, calendar });
} catch {
continue;
@@ -216,7 +247,7 @@ export const useCalendar = create<CalendarState>((set, get) => ({
const accountId = get().accountId;
if (!accountId) return;
try {
const res = await client.call<GetResponse<Calendar>>("Calendar/get", { accountId, ids: null });
const res = await client.call<GetResponse<Calendar>>("Calendar/get", { accountId, ids: null, properties: CALENDAR_PROPS });
const calendars: Record<Id, Calendar> = {};
for (const c of res.list) calendars[c.id] = c;
set({ calendars, error: null });
+11 -2
View File
@@ -14,6 +14,15 @@ export interface Suggestion {
photo?: string | null;
}
/*
* Asked for by name: `shareWith` is not returned by default.
*
* An `AddressBook/get` with no `properties` omits it entirely -- confirmed
* against 0.16.19 on 2026-08-27 on a book that really was shared. See the note
* on CALENDAR_PROPS; both had the same hole and Files did not.
*/
export const ADDRESS_BOOK_PROPS = ["id", "name", "description", "sortOrder", "isDefault", "isSubscribed", "shareWith", "myRights"];
/** A book somebody else shared, and the account it lives in. */
export interface SharedBook {
accountId: Id;
@@ -132,7 +141,7 @@ export const useContacts = create<ContactsState>((set, get) => ({
const cards: Record<string, ContactCard> = {};
for (const [accountId, account] of accounts) {
try {
const res = await client.call<GetResponse<AddressBook>>("AddressBook/get", { accountId, ids: null });
const res = await client.call<GetResponse<AddressBook>>("AddressBook/get", { accountId, ids: null, properties: ADDRESS_BOOK_PROPS });
for (const book of res.list) books.push({ accountId, accountName: account.name, book });
/*
* Cards come only from books the reader has added.
@@ -223,7 +232,7 @@ export const useContacts = create<ContactsState>((set, get) => ({
const accountId = get().accountId;
if (!accountId) return;
try {
const res = await client.call<GetResponse<AddressBook>>("AddressBook/get", { accountId, ids: null });
const res = await client.call<GetResponse<AddressBook>>("AddressBook/get", { accountId, ids: null, properties: ADDRESS_BOOK_PROPS });
const books: Record<Id, AddressBook> = {};
for (const b of res.list) books[b.id] = b;
set({ books, error: null });