@openmarket/rooms-client 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/sidebar-model.d.ts +3 -0
- package/dist/agent/sidebar-model.js +31 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +25 -16
- package/dist/social-client.d.ts +6 -0
- package/dist/social-client.js +8 -0
- package/package.json +1 -1
- package/src/agent/sidebar-model.ts +28 -0
- package/src/client.ts +36 -21
- package/src/social-client.ts +12 -0
|
@@ -74,6 +74,9 @@ export declare class SidebarModel {
|
|
|
74
74
|
constructor(deps: SidebarModelDeps);
|
|
75
75
|
snapshot(): SidebarSnapshot;
|
|
76
76
|
onUpdate(listener: () => void): () => void;
|
|
77
|
+
/** Apply a same-user realtime DM counter without refetching the full room
|
|
78
|
+
* directory. Username is the stable DM key used by the sidebar model. */
|
|
79
|
+
applyDmUnread(username: string, unread: number): void;
|
|
77
80
|
/**
|
|
78
81
|
* Post-mutation refresh: if a poll-triggered load is already in flight it
|
|
79
82
|
* may predate the caller's writes, so wait it out and load AGAIN. Readers
|
|
@@ -112,6 +112,34 @@ export class SidebarModel {
|
|
|
112
112
|
this.listeners.delete(listener);
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
|
+
/** Apply a same-user realtime DM counter without refetching the full room
|
|
116
|
+
* directory. Username is the stable DM key used by the sidebar model. */
|
|
117
|
+
applyDmUnread(username, unread) {
|
|
118
|
+
const key = normalizeDmUsername(username);
|
|
119
|
+
if (!key)
|
|
120
|
+
return;
|
|
121
|
+
const normalizedUnread = Number.isFinite(unread) ? Math.max(0, Math.floor(unread)) : 0;
|
|
122
|
+
let found = false;
|
|
123
|
+
let changed = false;
|
|
124
|
+
const next = this.snapshotValue.dms.map((dm) => {
|
|
125
|
+
if (normalizeDmUsername(dm.username) !== key)
|
|
126
|
+
return dm;
|
|
127
|
+
found = true;
|
|
128
|
+
if (dm.unread === normalizedUnread)
|
|
129
|
+
return dm;
|
|
130
|
+
changed = true;
|
|
131
|
+
return { ...dm, unread: normalizedUnread };
|
|
132
|
+
});
|
|
133
|
+
if (!found && normalizedUnread > 0) {
|
|
134
|
+
next.push({ username: username.replace(/^@+/, ""), unread: normalizedUnread });
|
|
135
|
+
changed = true;
|
|
136
|
+
}
|
|
137
|
+
if (!changed)
|
|
138
|
+
return;
|
|
139
|
+
this.snapshotValue = { ...this.snapshotValue, dms: next };
|
|
140
|
+
for (const listener of this.listeners)
|
|
141
|
+
listener();
|
|
142
|
+
}
|
|
115
143
|
/**
|
|
116
144
|
* Post-mutation refresh: if a poll-triggered load is already in flight it
|
|
117
145
|
* may predate the caller's writes, so wait it out and load AGAIN. Readers
|
|
@@ -160,3 +188,6 @@ export class SidebarModel {
|
|
|
160
188
|
});
|
|
161
189
|
}
|
|
162
190
|
}
|
|
191
|
+
function normalizeDmUsername(username) {
|
|
192
|
+
return username.trim().replace(/^@+/, "").toLowerCase();
|
|
193
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -242,6 +242,9 @@ export { workspaceRoomName } from "./names.js";
|
|
|
242
242
|
export declare function listRooms(config: RoomClientConfig, request?: RoomListRequest): Promise<RoomListResult>;
|
|
243
243
|
export declare function createRoom(config: RoomClientConfig, request: RoomCreateRequest): Promise<RoomCreateResult>;
|
|
244
244
|
export declare function inviteRoom(config: RoomClientConfig, request: RoomInviteRequest): Promise<RoomInviteResult>;
|
|
245
|
+
/** Invite over an already-authenticated socket. Long-lived GUI/TUI sessions
|
|
246
|
+
* use this path so a one-shot invite cannot create a second connection. */
|
|
247
|
+
export declare function inviteRoomOnClient(client: RoomWsClient, request: RoomInviteRequest, timeoutMs?: number): Promise<RoomInvitedPayload>;
|
|
245
248
|
export declare function removeRoomMember(config: RoomClientConfig, request: RoomRemoveMemberRequest): Promise<RoomRemoveMemberResult>;
|
|
246
249
|
export declare function reportRoomTarget(config: RoomClientConfig, request: RoomReportRequest): Promise<RoomReportResult>;
|
|
247
250
|
export declare function muteRoomMember(config: RoomClientConfig, request: RoomMuteRequest): Promise<RoomMuteResult>;
|
package/dist/client.js
CHANGED
|
@@ -101,9 +101,7 @@ async function createRoomViaWebSocket(config, request) {
|
|
|
101
101
|
export async function inviteRoom(config, request) {
|
|
102
102
|
// `@user` and `user` are the same person: strip the typing syntax once,
|
|
103
103
|
// before either transport (the relay resolves the bare name).
|
|
104
|
-
const normalized = request
|
|
105
|
-
? { ...request, username: request.username.trim().replace(/^@+/, "") }
|
|
106
|
-
: request;
|
|
104
|
+
const normalized = normalizeRoomInviteRequest(request);
|
|
107
105
|
if (!config.webSocketCtor) {
|
|
108
106
|
try {
|
|
109
107
|
return await inviteRoomViaHttp(config, normalized);
|
|
@@ -119,24 +117,35 @@ async function inviteRoomViaWebSocket(config, request) {
|
|
|
119
117
|
const client = new RoomWsClient(config);
|
|
120
118
|
try {
|
|
121
119
|
const auth = await client.connect();
|
|
122
|
-
|
|
123
|
-
client.send({
|
|
124
|
-
type: RoomClientMessageType.INVITE,
|
|
125
|
-
payload: {
|
|
126
|
-
requestId,
|
|
127
|
-
room: request.room,
|
|
128
|
-
...(request.username ? { username: request.username } : {}),
|
|
129
|
-
...(request.userId ? { userId: request.userId } : {}),
|
|
130
|
-
},
|
|
131
|
-
timestamp: Date.now(),
|
|
132
|
-
});
|
|
133
|
-
const invited = await client.waitFor(RoomServerMessageType.ROOM_INVITED, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
134
|
-
return { auth, invite: invited.payload };
|
|
120
|
+
return { auth, invite: await inviteRoomOnClient(client, request) };
|
|
135
121
|
}
|
|
136
122
|
finally {
|
|
137
123
|
client.close();
|
|
138
124
|
}
|
|
139
125
|
}
|
|
126
|
+
/** Invite over an already-authenticated socket. Long-lived GUI/TUI sessions
|
|
127
|
+
* use this path so a one-shot invite cannot create a second connection. */
|
|
128
|
+
export async function inviteRoomOnClient(client, request, timeoutMs) {
|
|
129
|
+
const normalized = normalizeRoomInviteRequest(request);
|
|
130
|
+
const requestId = randomUUID();
|
|
131
|
+
client.send({
|
|
132
|
+
type: RoomClientMessageType.INVITE,
|
|
133
|
+
payload: {
|
|
134
|
+
requestId,
|
|
135
|
+
room: normalized.room,
|
|
136
|
+
...(normalized.username ? { username: normalized.username } : {}),
|
|
137
|
+
...(normalized.userId ? { userId: normalized.userId } : {}),
|
|
138
|
+
},
|
|
139
|
+
timestamp: Date.now(),
|
|
140
|
+
});
|
|
141
|
+
const invited = await client.waitFor(RoomServerMessageType.ROOM_INVITED, (msg) => msg.payload.requestId === requestId, timeoutMs, requestId);
|
|
142
|
+
return invited.payload;
|
|
143
|
+
}
|
|
144
|
+
function normalizeRoomInviteRequest(request) {
|
|
145
|
+
return request.username
|
|
146
|
+
? { ...request, username: request.username.trim().replace(/^@+/, "") }
|
|
147
|
+
: request;
|
|
148
|
+
}
|
|
140
149
|
export async function removeRoomMember(config, request) {
|
|
141
150
|
if (!config.webSocketCtor) {
|
|
142
151
|
try {
|
package/dist/social-client.d.ts
CHANGED
|
@@ -330,6 +330,12 @@ export declare function listRoomChannelMembers(config: RoomSocialConfig, roomId:
|
|
|
330
330
|
export declare function removeSpaceMember(config: RoomSocialConfig, spaceId: string, targetUserId: string): Promise<{
|
|
331
331
|
removed?: boolean;
|
|
332
332
|
}>;
|
|
333
|
+
/** Leave a server as the authenticated caller. This is deliberately separate
|
|
334
|
+
* from the manager-only member-removal route so clients never send their own
|
|
335
|
+
* userId as authorization data. */
|
|
336
|
+
export declare function leaveSpace(config: RoomSocialConfig, spaceId: string): Promise<{
|
|
337
|
+
left?: boolean;
|
|
338
|
+
}>;
|
|
333
339
|
export declare function changeSpaceMemberRole(config: RoomSocialConfig, spaceId: string, targetUserId: string, role: "admin" | "librarian" | "member"): Promise<RoomSpaceMember>;
|
|
334
340
|
/** Set (string) or clear (null) a member's per-space nickname; the store validates and enforces uniqueness. */
|
|
335
341
|
export declare function setSpaceMemberNickname(config: RoomSocialConfig, spaceId: string, targetUserId: string, nickname: string | null): Promise<RoomSpaceMember>;
|
package/dist/social-client.js
CHANGED
|
@@ -500,6 +500,14 @@ export async function listRoomChannelMembers(config, roomId, options = {}) {
|
|
|
500
500
|
export async function removeSpaceMember(config, spaceId, targetUserId) {
|
|
501
501
|
return socialRequest(config, `/spaces/${encodeURIComponent(spaceId)}/members/${encodeURIComponent(targetUserId)}`, { method: "DELETE" });
|
|
502
502
|
}
|
|
503
|
+
/** Leave a server as the authenticated caller. This is deliberately separate
|
|
504
|
+
* from the manager-only member-removal route so clients never send their own
|
|
505
|
+
* userId as authorization data. */
|
|
506
|
+
export async function leaveSpace(config, spaceId) {
|
|
507
|
+
return socialRequest(config, `/spaces/${encodeURIComponent(spaceId)}/membership`, {
|
|
508
|
+
method: "DELETE",
|
|
509
|
+
});
|
|
510
|
+
}
|
|
503
511
|
export async function changeSpaceMemberRole(config, spaceId, targetUserId, role) {
|
|
504
512
|
const result = await socialRequest(config, `/spaces/${encodeURIComponent(spaceId)}/members/${encodeURIComponent(targetUserId)}/role`, { method: "PATCH", body: { role } });
|
|
505
513
|
return result.member;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmarket/rooms-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "OM Rooms protocol client: wire types, WebSocket + REST clients, and chat view-models. Browser-safe (no node:/bun: imports).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -186,6 +186,30 @@ export class SidebarModel {
|
|
|
186
186
|
};
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
/** Apply a same-user realtime DM counter without refetching the full room
|
|
190
|
+
* directory. Username is the stable DM key used by the sidebar model. */
|
|
191
|
+
applyDmUnread(username: string, unread: number): void {
|
|
192
|
+
const key = normalizeDmUsername(username);
|
|
193
|
+
if (!key) return;
|
|
194
|
+
const normalizedUnread = Number.isFinite(unread) ? Math.max(0, Math.floor(unread)) : 0;
|
|
195
|
+
let found = false;
|
|
196
|
+
let changed = false;
|
|
197
|
+
const next = this.snapshotValue.dms.map((dm) => {
|
|
198
|
+
if (normalizeDmUsername(dm.username) !== key) return dm;
|
|
199
|
+
found = true;
|
|
200
|
+
if (dm.unread === normalizedUnread) return dm;
|
|
201
|
+
changed = true;
|
|
202
|
+
return { ...dm, unread: normalizedUnread };
|
|
203
|
+
});
|
|
204
|
+
if (!found && normalizedUnread > 0) {
|
|
205
|
+
next.push({ username: username.replace(/^@+/, ""), unread: normalizedUnread });
|
|
206
|
+
changed = true;
|
|
207
|
+
}
|
|
208
|
+
if (!changed) return;
|
|
209
|
+
this.snapshotValue = { ...this.snapshotValue, dms: next };
|
|
210
|
+
for (const listener of this.listeners) listener();
|
|
211
|
+
}
|
|
212
|
+
|
|
189
213
|
/**
|
|
190
214
|
* Post-mutation refresh: if a poll-triggered load is already in flight it
|
|
191
215
|
* may predate the caller's writes, so wait it out and load AGAIN. Readers
|
|
@@ -233,3 +257,7 @@ export class SidebarModel {
|
|
|
233
257
|
});
|
|
234
258
|
}
|
|
235
259
|
}
|
|
260
|
+
|
|
261
|
+
function normalizeDmUsername(username: string): string {
|
|
262
|
+
return username.trim().replace(/^@+/, "").toLowerCase();
|
|
263
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -443,9 +443,7 @@ export async function inviteRoom(
|
|
|
443
443
|
): Promise<RoomInviteResult> {
|
|
444
444
|
// `@user` and `user` are the same person: strip the typing syntax once,
|
|
445
445
|
// before either transport (the relay resolves the bare name).
|
|
446
|
-
const normalized
|
|
447
|
-
? { ...request, username: request.username.trim().replace(/^@+/, "") }
|
|
448
|
-
: request;
|
|
446
|
+
const normalized = normalizeRoomInviteRequest(request);
|
|
449
447
|
if (!config.webSocketCtor) {
|
|
450
448
|
try {
|
|
451
449
|
return await inviteRoomViaHttp(config, normalized);
|
|
@@ -463,29 +461,46 @@ async function inviteRoomViaWebSocket(
|
|
|
463
461
|
const client = new RoomWsClient(config);
|
|
464
462
|
try {
|
|
465
463
|
const auth = await client.connect();
|
|
466
|
-
|
|
467
|
-
client.send({
|
|
468
|
-
type: RoomClientMessageType.INVITE,
|
|
469
|
-
payload: {
|
|
470
|
-
requestId,
|
|
471
|
-
room: request.room,
|
|
472
|
-
...(request.username ? { username: request.username } : {}),
|
|
473
|
-
...(request.userId ? { userId: request.userId } : {}),
|
|
474
|
-
},
|
|
475
|
-
timestamp: Date.now(),
|
|
476
|
-
});
|
|
477
|
-
const invited = await client.waitFor(
|
|
478
|
-
RoomServerMessageType.ROOM_INVITED,
|
|
479
|
-
(msg) => msg.payload.requestId === requestId,
|
|
480
|
-
undefined,
|
|
481
|
-
requestId,
|
|
482
|
-
);
|
|
483
|
-
return { auth, invite: invited.payload };
|
|
464
|
+
return { auth, invite: await inviteRoomOnClient(client, request) };
|
|
484
465
|
} finally {
|
|
485
466
|
client.close();
|
|
486
467
|
}
|
|
487
468
|
}
|
|
488
469
|
|
|
470
|
+
/** Invite over an already-authenticated socket. Long-lived GUI/TUI sessions
|
|
471
|
+
* use this path so a one-shot invite cannot create a second connection. */
|
|
472
|
+
export async function inviteRoomOnClient(
|
|
473
|
+
client: RoomWsClient,
|
|
474
|
+
request: RoomInviteRequest,
|
|
475
|
+
timeoutMs?: number,
|
|
476
|
+
): Promise<RoomInvitedPayload> {
|
|
477
|
+
const normalized = normalizeRoomInviteRequest(request);
|
|
478
|
+
const requestId = randomUUID();
|
|
479
|
+
client.send({
|
|
480
|
+
type: RoomClientMessageType.INVITE,
|
|
481
|
+
payload: {
|
|
482
|
+
requestId,
|
|
483
|
+
room: normalized.room,
|
|
484
|
+
...(normalized.username ? { username: normalized.username } : {}),
|
|
485
|
+
...(normalized.userId ? { userId: normalized.userId } : {}),
|
|
486
|
+
},
|
|
487
|
+
timestamp: Date.now(),
|
|
488
|
+
});
|
|
489
|
+
const invited = await client.waitFor(
|
|
490
|
+
RoomServerMessageType.ROOM_INVITED,
|
|
491
|
+
(msg) => msg.payload.requestId === requestId,
|
|
492
|
+
timeoutMs,
|
|
493
|
+
requestId,
|
|
494
|
+
);
|
|
495
|
+
return invited.payload;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function normalizeRoomInviteRequest(request: RoomInviteRequest): RoomInviteRequest {
|
|
499
|
+
return request.username
|
|
500
|
+
? { ...request, username: request.username.trim().replace(/^@+/, "") }
|
|
501
|
+
: request;
|
|
502
|
+
}
|
|
503
|
+
|
|
489
504
|
export async function removeRoomMember(
|
|
490
505
|
config: RoomClientConfig,
|
|
491
506
|
request: RoomRemoveMemberRequest,
|
package/src/social-client.ts
CHANGED
|
@@ -1044,6 +1044,18 @@ export async function removeSpaceMember(
|
|
|
1044
1044
|
);
|
|
1045
1045
|
}
|
|
1046
1046
|
|
|
1047
|
+
/** Leave a server as the authenticated caller. This is deliberately separate
|
|
1048
|
+
* from the manager-only member-removal route so clients never send their own
|
|
1049
|
+
* userId as authorization data. */
|
|
1050
|
+
export async function leaveSpace(
|
|
1051
|
+
config: RoomSocialConfig,
|
|
1052
|
+
spaceId: string,
|
|
1053
|
+
): Promise<{ left?: boolean }> {
|
|
1054
|
+
return socialRequest(config, `/spaces/${encodeURIComponent(spaceId)}/membership`, {
|
|
1055
|
+
method: "DELETE",
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1047
1059
|
export async function changeSpaceMemberRole(
|
|
1048
1060
|
config: RoomSocialConfig,
|
|
1049
1061
|
spaceId: string,
|