@scalemule/chat 0.0.5 → 0.0.8

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.
@@ -0,0 +1,285 @@
1
+ import { EventEmitter } from './chunk-5O5YLRJL.js';
2
+
3
+ // src/shared/ChatController.ts
4
+ var ChatController = class extends EventEmitter {
5
+ constructor(client, conversationId) {
6
+ super();
7
+ this.typingTimers = /* @__PURE__ */ new Map();
8
+ this.unsubscribers = [];
9
+ this.client = client;
10
+ this.conversationId = conversationId;
11
+ this.state = {
12
+ conversationId,
13
+ messages: [],
14
+ readStatuses: [],
15
+ typingUsers: [],
16
+ members: [],
17
+ hasMore: false,
18
+ isLoading: true,
19
+ error: null
20
+ };
21
+ }
22
+ getState() {
23
+ return this.state;
24
+ }
25
+ async init(options = {}) {
26
+ const realtimeEnabled = options.realtime ?? true;
27
+ const presenceEnabled = options.presence ?? realtimeEnabled;
28
+ this.bindEvents();
29
+ if (realtimeEnabled) {
30
+ this.client.connect();
31
+ }
32
+ try {
33
+ await this.client.getConversation(this.conversationId);
34
+ const [messagesResult, readStatusResult] = await Promise.all([
35
+ this.client.getMessages(this.conversationId),
36
+ this.client.getReadStatus(this.conversationId)
37
+ ]);
38
+ this.state = {
39
+ ...this.state,
40
+ messages: messagesResult.data?.messages ?? [],
41
+ readStatuses: readStatusResult.data?.statuses ?? [],
42
+ hasMore: messagesResult.data?.has_more ?? false,
43
+ isLoading: false,
44
+ error: messagesResult.error?.message ?? readStatusResult.error?.message ?? null
45
+ };
46
+ if (realtimeEnabled) {
47
+ this.unsubscribers.push(this.client.subscribeToConversation(this.conversationId));
48
+ }
49
+ if (presenceEnabled) {
50
+ this.client.joinPresence(this.conversationId);
51
+ }
52
+ this.emit("state", this.state);
53
+ this.emit("ready", this.state);
54
+ return this.state;
55
+ } catch (error) {
56
+ const message = error instanceof Error ? error.message : "Failed to initialize chat controller";
57
+ this.state = {
58
+ ...this.state,
59
+ isLoading: false,
60
+ error: message
61
+ };
62
+ this.emit("state", this.state);
63
+ this.emit("error", { message });
64
+ throw error;
65
+ }
66
+ }
67
+ async loadMore() {
68
+ const oldestId = this.state.messages[0]?.id;
69
+ if (!oldestId) return;
70
+ const result = await this.client.getMessages(this.conversationId, { before: oldestId });
71
+ if (result.data?.messages?.length) {
72
+ this.state = {
73
+ ...this.state,
74
+ messages: [...result.data.messages, ...this.state.messages],
75
+ hasMore: result.data.has_more ?? false
76
+ };
77
+ this.emit("state", this.state);
78
+ }
79
+ }
80
+ async sendMessage(content, attachments = []) {
81
+ const messageType = attachments.length > 0 ? attachments.every((attachment) => attachment.mime_type.startsWith("image/")) && !content ? "image" : "file" : "text";
82
+ const result = await this.client.sendMessage(this.conversationId, {
83
+ content,
84
+ attachments,
85
+ message_type: messageType
86
+ });
87
+ if (result.error) {
88
+ throw new Error(result.error.message);
89
+ }
90
+ }
91
+ stageOptimisticMessage(message) {
92
+ const staged = this.client.stageOptimisticMessage(this.conversationId, message);
93
+ this.patchState({
94
+ messages: [...this.client.getCachedMessages(this.conversationId)]
95
+ });
96
+ return staged;
97
+ }
98
+ async uploadAttachment(file, onProgress, signal) {
99
+ return this.client.uploadAttachment(file, onProgress, signal);
100
+ }
101
+ async refreshAttachmentUrl(messageId, fileId) {
102
+ return this.client.refreshAttachmentUrl(messageId, fileId);
103
+ }
104
+ async addReaction(messageId, emoji) {
105
+ const result = await this.client.addReaction(messageId, emoji);
106
+ if (result.error) {
107
+ throw new Error(result.error.message);
108
+ }
109
+ }
110
+ async removeReaction(messageId, emoji) {
111
+ const result = await this.client.removeReaction(messageId, emoji);
112
+ if (result.error) {
113
+ throw new Error(result.error.message);
114
+ }
115
+ }
116
+ async reportMessage(messageId, reason, description) {
117
+ return this.client.reportMessage(messageId, reason, description);
118
+ }
119
+ async muteConversation(mutedUntil) {
120
+ return this.client.muteConversation(this.conversationId, mutedUntil);
121
+ }
122
+ async unmuteConversation() {
123
+ return this.client.unmuteConversation(this.conversationId);
124
+ }
125
+ async markRead() {
126
+ await this.client.markRead(this.conversationId);
127
+ }
128
+ async refreshReadStatus() {
129
+ const result = await this.client.getReadStatus(this.conversationId);
130
+ if (result.data?.statuses) {
131
+ this.patchState({ readStatuses: result.data.statuses });
132
+ return result.data.statuses;
133
+ }
134
+ if (result.error) {
135
+ throw new Error(result.error.message);
136
+ }
137
+ return this.state.readStatuses;
138
+ }
139
+ sendTyping(isTyping = true) {
140
+ void this.client.sendTyping(this.conversationId, isTyping);
141
+ }
142
+ destroy() {
143
+ this.client.leavePresence(this.conversationId);
144
+ for (const timer of this.typingTimers.values()) {
145
+ clearTimeout(timer);
146
+ }
147
+ this.typingTimers.clear();
148
+ for (const unsubscribe of this.unsubscribers) {
149
+ unsubscribe();
150
+ }
151
+ this.unsubscribers = [];
152
+ this.removeAllListeners();
153
+ }
154
+ bindEvents() {
155
+ if (this.unsubscribers.length) return;
156
+ this.unsubscribers.push(
157
+ this.client.on("message", ({ conversationId }) => {
158
+ if (conversationId !== this.conversationId) return;
159
+ this.patchState({
160
+ messages: [...this.client.getCachedMessages(this.conversationId)]
161
+ });
162
+ })
163
+ );
164
+ this.unsubscribers.push(
165
+ this.client.on("message:updated", ({ conversationId }) => {
166
+ if (conversationId !== this.conversationId) return;
167
+ this.patchState({
168
+ messages: [...this.client.getCachedMessages(this.conversationId)]
169
+ });
170
+ })
171
+ );
172
+ this.unsubscribers.push(
173
+ this.client.on("message:deleted", ({ conversationId }) => {
174
+ if (conversationId !== this.conversationId) return;
175
+ this.patchState({
176
+ messages: [...this.client.getCachedMessages(this.conversationId)]
177
+ });
178
+ })
179
+ );
180
+ this.unsubscribers.push(
181
+ this.client.on("reaction", ({ conversationId }) => {
182
+ if (conversationId !== this.conversationId) return;
183
+ this.patchState({
184
+ messages: [...this.client.getCachedMessages(this.conversationId)]
185
+ });
186
+ })
187
+ );
188
+ this.unsubscribers.push(
189
+ this.client.on("typing", ({ conversationId, userId }) => {
190
+ if (conversationId !== this.conversationId) return;
191
+ this.patchState({
192
+ typingUsers: this.state.typingUsers.includes(userId) ? this.state.typingUsers : [...this.state.typingUsers, userId]
193
+ });
194
+ const timer = this.typingTimers.get(userId);
195
+ if (timer) {
196
+ clearTimeout(timer);
197
+ }
198
+ this.typingTimers.set(
199
+ userId,
200
+ setTimeout(() => {
201
+ this.patchState({
202
+ typingUsers: this.state.typingUsers.filter((typingUserId) => typingUserId !== userId)
203
+ });
204
+ this.typingTimers.delete(userId);
205
+ }, 3e3)
206
+ );
207
+ })
208
+ );
209
+ this.unsubscribers.push(
210
+ this.client.on("typing:stop", ({ conversationId, userId }) => {
211
+ if (conversationId !== this.conversationId) return;
212
+ const timer = this.typingTimers.get(userId);
213
+ if (timer) {
214
+ clearTimeout(timer);
215
+ this.typingTimers.delete(userId);
216
+ }
217
+ this.patchState({
218
+ typingUsers: this.state.typingUsers.filter((typingUserId) => typingUserId !== userId)
219
+ });
220
+ })
221
+ );
222
+ this.unsubscribers.push(
223
+ this.client.on("read", ({ conversationId, userId, lastReadAt }) => {
224
+ if (conversationId !== this.conversationId) return;
225
+ const nextStatuses = [...this.state.readStatuses];
226
+ const existingIndex = nextStatuses.findIndex((status) => status.user_id === userId);
227
+ if (existingIndex >= 0) {
228
+ nextStatuses[existingIndex] = { ...nextStatuses[existingIndex], last_read_at: lastReadAt };
229
+ } else {
230
+ nextStatuses.push({ user_id: userId, last_read_at: lastReadAt });
231
+ }
232
+ this.patchState({ readStatuses: nextStatuses });
233
+ })
234
+ );
235
+ this.unsubscribers.push(
236
+ this.client.on("presence:state", ({ conversationId, members }) => {
237
+ if (conversationId !== this.conversationId) return;
238
+ this.patchState({
239
+ members: members.map((member) => ({
240
+ userId: member.user_id,
241
+ status: member.status ?? "online",
242
+ userData: member.user_data
243
+ }))
244
+ });
245
+ })
246
+ );
247
+ this.unsubscribers.push(
248
+ this.client.on("presence:join", ({ conversationId, userId, userData }) => {
249
+ if (conversationId !== this.conversationId) return;
250
+ if (this.state.members.some((member) => member.userId === userId)) return;
251
+ this.patchState({
252
+ members: [...this.state.members, { userId, status: "online", userData }]
253
+ });
254
+ })
255
+ );
256
+ this.unsubscribers.push(
257
+ this.client.on("presence:leave", ({ conversationId, userId }) => {
258
+ if (conversationId !== this.conversationId) return;
259
+ this.patchState({
260
+ members: this.state.members.filter((member) => member.userId !== userId)
261
+ });
262
+ })
263
+ );
264
+ this.unsubscribers.push(
265
+ this.client.on("presence:update", ({ conversationId, userId, status, userData }) => {
266
+ if (conversationId !== this.conversationId) return;
267
+ this.patchState({
268
+ members: this.state.members.map(
269
+ (member) => member.userId === userId ? { ...member, status, userData } : member
270
+ )
271
+ });
272
+ })
273
+ );
274
+ }
275
+ patchState(patch) {
276
+ this.state = {
277
+ ...this.state,
278
+ ...patch,
279
+ error: patch.error !== void 0 ? patch.error : this.state.error
280
+ };
281
+ this.emit("state", this.state);
282
+ }
283
+ };
284
+
285
+ export { ChatController };
@@ -0,0 +1,287 @@
1
+ 'use strict';
2
+
3
+ var chunkW2PWFS3E_cjs = require('./chunk-W2PWFS3E.cjs');
4
+
5
+ // src/shared/ChatController.ts
6
+ var ChatController = class extends chunkW2PWFS3E_cjs.EventEmitter {
7
+ constructor(client, conversationId) {
8
+ super();
9
+ this.typingTimers = /* @__PURE__ */ new Map();
10
+ this.unsubscribers = [];
11
+ this.client = client;
12
+ this.conversationId = conversationId;
13
+ this.state = {
14
+ conversationId,
15
+ messages: [],
16
+ readStatuses: [],
17
+ typingUsers: [],
18
+ members: [],
19
+ hasMore: false,
20
+ isLoading: true,
21
+ error: null
22
+ };
23
+ }
24
+ getState() {
25
+ return this.state;
26
+ }
27
+ async init(options = {}) {
28
+ const realtimeEnabled = options.realtime ?? true;
29
+ const presenceEnabled = options.presence ?? realtimeEnabled;
30
+ this.bindEvents();
31
+ if (realtimeEnabled) {
32
+ this.client.connect();
33
+ }
34
+ try {
35
+ await this.client.getConversation(this.conversationId);
36
+ const [messagesResult, readStatusResult] = await Promise.all([
37
+ this.client.getMessages(this.conversationId),
38
+ this.client.getReadStatus(this.conversationId)
39
+ ]);
40
+ this.state = {
41
+ ...this.state,
42
+ messages: messagesResult.data?.messages ?? [],
43
+ readStatuses: readStatusResult.data?.statuses ?? [],
44
+ hasMore: messagesResult.data?.has_more ?? false,
45
+ isLoading: false,
46
+ error: messagesResult.error?.message ?? readStatusResult.error?.message ?? null
47
+ };
48
+ if (realtimeEnabled) {
49
+ this.unsubscribers.push(this.client.subscribeToConversation(this.conversationId));
50
+ }
51
+ if (presenceEnabled) {
52
+ this.client.joinPresence(this.conversationId);
53
+ }
54
+ this.emit("state", this.state);
55
+ this.emit("ready", this.state);
56
+ return this.state;
57
+ } catch (error) {
58
+ const message = error instanceof Error ? error.message : "Failed to initialize chat controller";
59
+ this.state = {
60
+ ...this.state,
61
+ isLoading: false,
62
+ error: message
63
+ };
64
+ this.emit("state", this.state);
65
+ this.emit("error", { message });
66
+ throw error;
67
+ }
68
+ }
69
+ async loadMore() {
70
+ const oldestId = this.state.messages[0]?.id;
71
+ if (!oldestId) return;
72
+ const result = await this.client.getMessages(this.conversationId, { before: oldestId });
73
+ if (result.data?.messages?.length) {
74
+ this.state = {
75
+ ...this.state,
76
+ messages: [...result.data.messages, ...this.state.messages],
77
+ hasMore: result.data.has_more ?? false
78
+ };
79
+ this.emit("state", this.state);
80
+ }
81
+ }
82
+ async sendMessage(content, attachments = []) {
83
+ const messageType = attachments.length > 0 ? attachments.every((attachment) => attachment.mime_type.startsWith("image/")) && !content ? "image" : "file" : "text";
84
+ const result = await this.client.sendMessage(this.conversationId, {
85
+ content,
86
+ attachments,
87
+ message_type: messageType
88
+ });
89
+ if (result.error) {
90
+ throw new Error(result.error.message);
91
+ }
92
+ }
93
+ stageOptimisticMessage(message) {
94
+ const staged = this.client.stageOptimisticMessage(this.conversationId, message);
95
+ this.patchState({
96
+ messages: [...this.client.getCachedMessages(this.conversationId)]
97
+ });
98
+ return staged;
99
+ }
100
+ async uploadAttachment(file, onProgress, signal) {
101
+ return this.client.uploadAttachment(file, onProgress, signal);
102
+ }
103
+ async refreshAttachmentUrl(messageId, fileId) {
104
+ return this.client.refreshAttachmentUrl(messageId, fileId);
105
+ }
106
+ async addReaction(messageId, emoji) {
107
+ const result = await this.client.addReaction(messageId, emoji);
108
+ if (result.error) {
109
+ throw new Error(result.error.message);
110
+ }
111
+ }
112
+ async removeReaction(messageId, emoji) {
113
+ const result = await this.client.removeReaction(messageId, emoji);
114
+ if (result.error) {
115
+ throw new Error(result.error.message);
116
+ }
117
+ }
118
+ async reportMessage(messageId, reason, description) {
119
+ return this.client.reportMessage(messageId, reason, description);
120
+ }
121
+ async muteConversation(mutedUntil) {
122
+ return this.client.muteConversation(this.conversationId, mutedUntil);
123
+ }
124
+ async unmuteConversation() {
125
+ return this.client.unmuteConversation(this.conversationId);
126
+ }
127
+ async markRead() {
128
+ await this.client.markRead(this.conversationId);
129
+ }
130
+ async refreshReadStatus() {
131
+ const result = await this.client.getReadStatus(this.conversationId);
132
+ if (result.data?.statuses) {
133
+ this.patchState({ readStatuses: result.data.statuses });
134
+ return result.data.statuses;
135
+ }
136
+ if (result.error) {
137
+ throw new Error(result.error.message);
138
+ }
139
+ return this.state.readStatuses;
140
+ }
141
+ sendTyping(isTyping = true) {
142
+ void this.client.sendTyping(this.conversationId, isTyping);
143
+ }
144
+ destroy() {
145
+ this.client.leavePresence(this.conversationId);
146
+ for (const timer of this.typingTimers.values()) {
147
+ clearTimeout(timer);
148
+ }
149
+ this.typingTimers.clear();
150
+ for (const unsubscribe of this.unsubscribers) {
151
+ unsubscribe();
152
+ }
153
+ this.unsubscribers = [];
154
+ this.removeAllListeners();
155
+ }
156
+ bindEvents() {
157
+ if (this.unsubscribers.length) return;
158
+ this.unsubscribers.push(
159
+ this.client.on("message", ({ conversationId }) => {
160
+ if (conversationId !== this.conversationId) return;
161
+ this.patchState({
162
+ messages: [...this.client.getCachedMessages(this.conversationId)]
163
+ });
164
+ })
165
+ );
166
+ this.unsubscribers.push(
167
+ this.client.on("message:updated", ({ conversationId }) => {
168
+ if (conversationId !== this.conversationId) return;
169
+ this.patchState({
170
+ messages: [...this.client.getCachedMessages(this.conversationId)]
171
+ });
172
+ })
173
+ );
174
+ this.unsubscribers.push(
175
+ this.client.on("message:deleted", ({ conversationId }) => {
176
+ if (conversationId !== this.conversationId) return;
177
+ this.patchState({
178
+ messages: [...this.client.getCachedMessages(this.conversationId)]
179
+ });
180
+ })
181
+ );
182
+ this.unsubscribers.push(
183
+ this.client.on("reaction", ({ conversationId }) => {
184
+ if (conversationId !== this.conversationId) return;
185
+ this.patchState({
186
+ messages: [...this.client.getCachedMessages(this.conversationId)]
187
+ });
188
+ })
189
+ );
190
+ this.unsubscribers.push(
191
+ this.client.on("typing", ({ conversationId, userId }) => {
192
+ if (conversationId !== this.conversationId) return;
193
+ this.patchState({
194
+ typingUsers: this.state.typingUsers.includes(userId) ? this.state.typingUsers : [...this.state.typingUsers, userId]
195
+ });
196
+ const timer = this.typingTimers.get(userId);
197
+ if (timer) {
198
+ clearTimeout(timer);
199
+ }
200
+ this.typingTimers.set(
201
+ userId,
202
+ setTimeout(() => {
203
+ this.patchState({
204
+ typingUsers: this.state.typingUsers.filter((typingUserId) => typingUserId !== userId)
205
+ });
206
+ this.typingTimers.delete(userId);
207
+ }, 3e3)
208
+ );
209
+ })
210
+ );
211
+ this.unsubscribers.push(
212
+ this.client.on("typing:stop", ({ conversationId, userId }) => {
213
+ if (conversationId !== this.conversationId) return;
214
+ const timer = this.typingTimers.get(userId);
215
+ if (timer) {
216
+ clearTimeout(timer);
217
+ this.typingTimers.delete(userId);
218
+ }
219
+ this.patchState({
220
+ typingUsers: this.state.typingUsers.filter((typingUserId) => typingUserId !== userId)
221
+ });
222
+ })
223
+ );
224
+ this.unsubscribers.push(
225
+ this.client.on("read", ({ conversationId, userId, lastReadAt }) => {
226
+ if (conversationId !== this.conversationId) return;
227
+ const nextStatuses = [...this.state.readStatuses];
228
+ const existingIndex = nextStatuses.findIndex((status) => status.user_id === userId);
229
+ if (existingIndex >= 0) {
230
+ nextStatuses[existingIndex] = { ...nextStatuses[existingIndex], last_read_at: lastReadAt };
231
+ } else {
232
+ nextStatuses.push({ user_id: userId, last_read_at: lastReadAt });
233
+ }
234
+ this.patchState({ readStatuses: nextStatuses });
235
+ })
236
+ );
237
+ this.unsubscribers.push(
238
+ this.client.on("presence:state", ({ conversationId, members }) => {
239
+ if (conversationId !== this.conversationId) return;
240
+ this.patchState({
241
+ members: members.map((member) => ({
242
+ userId: member.user_id,
243
+ status: member.status ?? "online",
244
+ userData: member.user_data
245
+ }))
246
+ });
247
+ })
248
+ );
249
+ this.unsubscribers.push(
250
+ this.client.on("presence:join", ({ conversationId, userId, userData }) => {
251
+ if (conversationId !== this.conversationId) return;
252
+ if (this.state.members.some((member) => member.userId === userId)) return;
253
+ this.patchState({
254
+ members: [...this.state.members, { userId, status: "online", userData }]
255
+ });
256
+ })
257
+ );
258
+ this.unsubscribers.push(
259
+ this.client.on("presence:leave", ({ conversationId, userId }) => {
260
+ if (conversationId !== this.conversationId) return;
261
+ this.patchState({
262
+ members: this.state.members.filter((member) => member.userId !== userId)
263
+ });
264
+ })
265
+ );
266
+ this.unsubscribers.push(
267
+ this.client.on("presence:update", ({ conversationId, userId, status, userData }) => {
268
+ if (conversationId !== this.conversationId) return;
269
+ this.patchState({
270
+ members: this.state.members.map(
271
+ (member) => member.userId === userId ? { ...member, status, userData } : member
272
+ )
273
+ });
274
+ })
275
+ );
276
+ }
277
+ patchState(patch) {
278
+ this.state = {
279
+ ...this.state,
280
+ ...patch,
281
+ error: patch.error !== void 0 ? patch.error : this.state.error
282
+ };
283
+ this.emit("state", this.state);
284
+ }
285
+ };
286
+
287
+ exports.ChatController = ChatController;