@vibexnpm/talkx 8.4.1 → 8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibexnpm/talkx",
3
- "version": "8.4.1",
3
+ "version": "8.5.0",
4
4
  "type": "module",
5
5
  "description": "TalkFlow SDK - Chat & WebRTC unified client",
6
6
  "main": "dist/talkflow-sdk.umd.js",
@@ -49,6 +49,9 @@ class ChatClient extends EventEmitter {
49
49
  // 채팅방 리스트 구독 상태 (카톡 스타일 리스트 실시간 업데이트)
50
50
  this.roomListSubscribed = false;
51
51
 
52
+ // Gmail 메일함 변화 구독 상태 — 자동 구독하지 않는다(Gmail 을 연결한 화면에서 subscribeMail()).
53
+ this.mailSubscribed = false;
54
+
52
55
  // 현재 보고 있는 채팅방 ID (이 방에서만 자동 읽음 처리)
53
56
  this._activeRoomId = null;
54
57
 
@@ -2732,6 +2735,61 @@ class ChatClient extends EventEmitter {
2732
2735
  return this.roomListSubscribed;
2733
2736
  }
2734
2737
 
2738
+ /**
2739
+ * Gmail 메일함 변화 구독 — {@code /user/queue/mail}. 받은 이벤트를 {@code mailChanged} 로 그대로 낸다.
2740
+ *
2741
+ * <p>새 메일 알림은 최상위 클라이언트의 {@code newMail} 을 쓴다 — 웹소켓 이벤트의 미리보기와 포그라운드 FCM 메일 푸시를
2742
+ * 합쳐 메일마다 한 번만 낸다. 재연결 때는 연결 관리자가 구독을 되살린다(이벤트는 최대 한 번 전달이라 재연결·창 포커스 때
2743
+ * 안읽은 수와 목록을 다시 읽는다).</p>
2744
+ */
2745
+ async subscribeMail() {
2746
+ if (this.mailSubscribed) {
2747
+ this.logger.debug('Already subscribed to mail');
2748
+ return;
2749
+ }
2750
+
2751
+ await this.connectionManager.subscribe(WebSocketPaths.MAIL_USER_DESTINATION, (event) => {
2752
+ this._handleMailEvent(event);
2753
+ });
2754
+
2755
+ this.mailSubscribed = true;
2756
+ this.logger.info('Subscribed to mail');
2757
+ this.emit('mailSubscribed', {});
2758
+ }
2759
+
2760
+ /**
2761
+ * Gmail 메일함 변화 구독 해제.
2762
+ */
2763
+ unsubscribeMail() {
2764
+ if (!this.mailSubscribed) {
2765
+ return;
2766
+ }
2767
+
2768
+ this.connectionManager.unsubscribe(WebSocketPaths.MAIL_USER_DESTINATION);
2769
+ this.mailSubscribed = false;
2770
+
2771
+ this.logger.info('Unsubscribed from mail');
2772
+ this.emit('mailUnsubscribed', {});
2773
+ }
2774
+
2775
+ /**
2776
+ * Gmail 메일함 변화 구독 여부.
2777
+ * @returns {boolean}
2778
+ */
2779
+ isMailSubscribed() {
2780
+ return this.mailSubscribed;
2781
+ }
2782
+
2783
+ /** 메일 이벤트 — 보낸 사람·제목이 실리므로 내용은 로그에 남기지 않는다(연결 ID·건수만). */
2784
+ _handleMailEvent(event) {
2785
+ if (!event || event.eventType !== 'MAIL_CHANGED') {
2786
+ return;
2787
+ }
2788
+ this.logger.debug('Mail changed:', event.connectionId,
2789
+ Array.isArray(event.messages) ? event.messages.length : 0);
2790
+ this.emit('mailChanged', event);
2791
+ }
2792
+
2735
2793
  /**
2736
2794
  * 메시지 읽음 처리
2737
2795
  * @param {string} roomId - 채팅방 ID
package/src/constants.js CHANGED
@@ -384,6 +384,10 @@ export const WebSocketPaths = {
384
384
  // 클라이언트는 "/user/queue/rooms" 구독 (Spring STOMP 가 세션별 자동 격리)
385
385
  ROOM_LIST_USER_DESTINATION: '/user/queue/rooms',
386
386
 
387
+ // Gmail 메일함 변화 (서버 GMAIL_MAILBOX_FEATURES_DESIGN.md §6-6·§6-7)
388
+ // 서버가 convertAndSendToUser(userId, "/queue/mail", MAIL_CHANGED) 로 전송 — 새 메일이면 messages[] 미리보기 포함
389
+ MAIL_USER_DESTINATION: '/user/queue/mail',
390
+
387
391
  // WebRTC destinations
388
392
  getWebRTCDestination: (roomId) => `/topic/webrtc/${roomId}`,
389
393
  getWebRTCUserDestination: () => `/user/queue/webrtc`,
@@ -68,6 +68,10 @@ const PENDING_NAVIGATION_STORE_NAME = 'pendingNavigation';
68
68
  // SW 와 동일한 key 규칙 사용 — 멀티테넌트에서 다른 projectId 의 pending 을 건드리지 않도록 한다.
69
69
  const PENDING_NAVIGATION_KEY_PREFIX = 'pending-room';
70
70
  const PENDING_NAVIGATION_LEGACY_KEY = 'pending-room';
71
+ // Gmail 메일 알림 클릭 — 방과 다른 key 라 consumePendingRoom 과 섞이지 않는다. 프로젝트별로만 저장한다(레거시 전역 key 없음).
72
+ const PENDING_MAIL_KEY_PREFIX = 'pending-mail';
73
+ // 앱이 이 시간 안에 꺼내지 않은 메일 이동은 버린다 — 며칠 뒤 앱을 열 때 엉뚱하게 메일 화면으로 가지 않게.
74
+ const PENDING_MAIL_MAX_AGE_MS = 10 * 60 * 1000;
71
75
 
72
76
  // deviceId 저장 — 같은 IndexedDB 의 별도 store 사용 (XSS 방어 + 기존 인프라 재사용).
73
77
  // DB 버전은 2 로 bump — v1 에서 pendingNavigation 만 있던 상태에서 deviceInfo store 추가.
@@ -175,6 +179,54 @@ function generateDeviceId() {
175
179
  return 'web-' + random;
176
180
  }
177
181
 
182
+ /**
183
+ * SW 가 저장한 메일 이동(알림 클릭 → 열린 창 없음)을 꺼내고 지운다. 같은 프로젝트·10분 안의 것만.
184
+ *
185
+ * @returns {Promise<{connectionId: string, messageId: (string|null)}|null>}
186
+ */
187
+ async function consumePendingMailFromIndexedDB(projectId) {
188
+ if (!projectId || typeof projectId !== 'string' || projectId.trim() === '') {
189
+ return null;
190
+ }
191
+ const db = await openPendingNavigationDb();
192
+ if (!db) {
193
+ return null;
194
+ }
195
+ const key = PENDING_MAIL_KEY_PREFIX + ':' + projectId;
196
+ return new Promise((resolve, reject) => {
197
+ const transaction = db.transaction(PENDING_NAVIGATION_STORE_NAME, 'readwrite');
198
+ const store = transaction.objectStore(PENDING_NAVIGATION_STORE_NAME);
199
+ let pending = null;
200
+ const request = store.get(key);
201
+ request.onsuccess = () => {
202
+ const record = request.result;
203
+ if (!record) {
204
+ return;
205
+ }
206
+ store.delete(key);
207
+ const fresh = typeof record.savedAt === 'number' && Date.now() - record.savedAt <= PENDING_MAIL_MAX_AGE_MS;
208
+ if (fresh && record.projectId === projectId && record.connectionId) {
209
+ pending = { connectionId: record.connectionId, messageId: record.messageId || null };
210
+ }
211
+ };
212
+ request.onerror = () => {
213
+ reject(request.error || new Error('IndexedDB read failed'));
214
+ };
215
+ transaction.oncomplete = () => {
216
+ db.close();
217
+ resolve(pending);
218
+ };
219
+ transaction.onerror = () => {
220
+ db.close();
221
+ reject(transaction.error || new Error('IndexedDB transaction failed'));
222
+ };
223
+ transaction.onabort = () => {
224
+ db.close();
225
+ reject(transaction.error || new Error('IndexedDB transaction aborted'));
226
+ };
227
+ });
228
+ }
229
+
178
230
  async function consumePendingRoomFromIndexedDB(projectId) {
179
231
  const db = await openPendingNavigationDb();
180
232
  if (!db) {
@@ -302,6 +354,17 @@ class PushManager {
302
354
  return consumePendingRoomFromIndexedDB(projectId);
303
355
  }
304
356
 
357
+ /**
358
+ * 서비스 워커가 cold start 시 저장한 메일 이동(Gmail 메일 알림 클릭)을 꺼내고 지운다.
359
+ * 앱 부팅 때 호출해 메일 화면으로 라우팅한다 — {@code messageId} 가 있으면 그 메일 상세, 없으면(묶음 알림) 받은메일함.
360
+ *
361
+ * @param {string} projectId
362
+ * @returns {Promise<{connectionId: string, messageId: (string|null)}|null>}
363
+ */
364
+ static async consumePendingMail(projectId) {
365
+ return consumePendingMailFromIndexedDB(projectId);
366
+ }
367
+
305
368
  /**
306
369
  * 현재 브라우저의 푸시 권한 상태를 조회한다.
307
370
  *
@@ -669,6 +732,15 @@ class PushManager {
669
732
  return PushManager.consumePendingRoom(this.projectId);
670
733
  }
671
734
 
735
+ /**
736
+ * 서비스 워커가 cold start 시 저장한 메일 이동을 꺼내고 지운다 — {@link PushManager.consumePendingMail}.
737
+ *
738
+ * @returns {Promise<{connectionId: string, messageId: (string|null)}|null>}
739
+ */
740
+ async consumePendingMail() {
741
+ return PushManager.consumePendingMail(this.projectId);
742
+ }
743
+
672
744
  /**
673
745
  * 현재 푸시 상태를 정리한다.
674
746
  * 로그아웃 또는 다른 사용자로 전환할 때 foreground listener 와 활성화 상태를 초기화한다.
@@ -51,6 +51,9 @@ const CHAT_DELEGATE_METHODS = [
51
51
  'subscribeRoomList',
52
52
  'unsubscribeRoomList',
53
53
  'isRoomListSubscribed',
54
+ 'subscribeMail',
55
+ 'unsubscribeMail',
56
+ 'isMailSubscribed',
54
57
  'getSubscribedRooms',
55
58
  'isSubscribed',
56
59
  'startTyping',
@@ -1,3 +1,5 @@
1
+ import { emitNewMailFromSocket } from './mailNotifications.js';
2
+
1
3
  const CONNECTION_EVENT_MAP = [
2
4
  ['connected', 'connected'],
3
5
  ['disconnected', 'disconnected'],
@@ -49,6 +51,10 @@ const CHAT_EVENT_MAP = [
49
51
  ['roomListDeleted', 'roomListDeleted'],
50
52
  ['roomListRoomUpdated', 'roomListRoomUpdated'],
51
53
  ['roomListUnreadUpdated', 'roomListUnreadUpdated'],
54
+ // Gmail 메일함 — mailChanged 는 받은 그대로. 새 메일 알림(newMail)은 아래에서 푸시와 합쳐 따로 낸다.
55
+ ['mailSubscribed', 'mailSubscribed'],
56
+ ['mailUnsubscribed', 'mailUnsubscribed'],
57
+ ['mailChanged', 'mailChanged'],
52
58
  ['retentionCleanup', 'retentionCleanup']
53
59
  ];
54
60
 
@@ -102,6 +108,7 @@ export function setupEventForwarding(client) {
102
108
 
103
109
  if (client.chat) {
104
110
  forwardMappedEvents(client.chat, client, CHAT_EVENT_MAP);
111
+ client.chat.on('mailChanged', (event) => emitNewMailFromSocket(client, event));
105
112
  }
106
113
 
107
114
  if (client.webrtc) {
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Gmail 새 메일 알림 — 웹소켓 {@code MAIL_CHANGED} 의 미리보기와 포그라운드 FCM 메일 푸시를 합쳐 메일마다 한 번만
3
+ * {@code newMail} 을 낸다 (서버 GMAIL_MAILBOX_FEATURES_DESIGN.md §6-7).
4
+ *
5
+ * <p>같은 메일이 두 길로 온다: 서버는 앱 접속 여부와 상관없이 FCM 을 보내고(채팅과 같음), 화면이 떠 있으면 웹소켓
6
+ * 이벤트도 받는다. 중복 판정 키는 {@code projectId:connectionId:messageId} — 어느 쪽이 먼저 와도 두 번째는 버린다.
7
+ * 묶음 푸시({@code kind=SUMMARY})도 묶은 메시지 ID({@code data.messageIds})로 같은 판정을 하고, 처음 보는 메일이 있을 때만
8
+ * 그 수로 {@code newMailSummary} 를 낸다(같은 묶음이 다시 와도, 개별 알림을 이미 받았어도 다시 띄우지 않는다).</p>
9
+ *
10
+ * <p>보낸 사람·제목은 로그에 남기지 않는다.</p>
11
+ */
12
+
13
+ /** 같은 메일을 다시 보지 않는 시간 — 서버 푸시 수명(수신 후 30분)보다 길게. */
14
+ const DEDUP_TTL_MS = 60 * 60 * 1000;
15
+ /** 기억하는 메일 수 상한 — 넘으면 오래된 것부터 잊는다. */
16
+ const DEDUP_MAX = 1000;
17
+
18
+ export function createMailNotificationDeduper({ ttlMs = DEDUP_TTL_MS, max = DEDUP_MAX, now = () => Date.now() } = {}) {
19
+ const seen = new Map();
20
+ return {
21
+ /** 처음 보는 키면 true(그리고 기억), 이미 본 키면 false. */
22
+ accept(key) {
23
+ const at = now();
24
+ for (const [seenKey, seenAt] of seen) {
25
+ if (at - seenAt < ttlMs) {
26
+ break; // Map 은 넣은 순서 — 첫 살아 있는 항목부터는 다 살아 있다
27
+ }
28
+ seen.delete(seenKey);
29
+ }
30
+ if (seen.has(key)) {
31
+ return false;
32
+ }
33
+ seen.set(key, at);
34
+ while (seen.size > max) {
35
+ seen.delete(seen.keys().next().value);
36
+ }
37
+ return true;
38
+ },
39
+ size() {
40
+ return seen.size;
41
+ }
42
+ };
43
+ }
44
+
45
+ function deduperOf(client) {
46
+ if (!client._mailNotificationDeduper) {
47
+ client._mailNotificationDeduper = createMailNotificationDeduper();
48
+ }
49
+ return client._mailNotificationDeduper;
50
+ }
51
+
52
+ function projectIdOf(client) {
53
+ return (client.options && client.options.projectId) || null;
54
+ }
55
+
56
+ function keyOf(projectId, connectionId, messageId) {
57
+ return `${projectId || ''}:${connectionId}:${messageId}`;
58
+ }
59
+
60
+ function senderOf(from) {
61
+ if (!from || typeof from !== 'object') {
62
+ return null;
63
+ }
64
+ return (typeof from.name === 'string' && from.name.trim() !== '') ? from.name : (from.email || null);
65
+ }
66
+
67
+ /**
68
+ * 웹소켓 {@code MAIL_CHANGED} → 새 메일마다 {@code newMail}. 읽음·보관 같은 변화(미리보기 없음)는 아무것도 내지 않는다.
69
+ */
70
+ export function emitNewMailFromSocket(client, event) {
71
+ if (!event || event.eventType !== 'MAIL_CHANGED' || !Array.isArray(event.messages) || !event.connectionId) {
72
+ return;
73
+ }
74
+ const projectId = projectIdOf(client);
75
+ const deduper = deduperOf(client);
76
+ for (const preview of event.messages) {
77
+ if (!preview || !preview.messageId) {
78
+ continue;
79
+ }
80
+ if (!deduper.accept(keyOf(projectId, event.connectionId, preview.messageId))) {
81
+ continue;
82
+ }
83
+ client.emit('newMail', {
84
+ projectId,
85
+ connectionId: event.connectionId,
86
+ messageId: preview.messageId,
87
+ threadId: preview.threadId || null,
88
+ sender: senderOf(preview.from),
89
+ from: preview.from || null,
90
+ subject: preview.subject || null,
91
+ receivedAt: preview.receivedAt || null,
92
+ source: 'socket'
93
+ });
94
+ }
95
+ }
96
+
97
+ /**
98
+ * 포그라운드 FCM 메일 푸시({@code data.target === 'MAIL'}) → {@code newMail}(한 통) 또는 {@code newMailSummary}(묶음).
99
+ * 메일 푸시면 {@code true} — 호출자는 {@code pushNotification} 을 내지 않는다(같은 알림을 두 번 띄우지 않게).
100
+ */
101
+ export function handleMailPush(client, payload) {
102
+ const data = (payload && payload.data) || {};
103
+ if (data.target !== 'MAIL') {
104
+ return false;
105
+ }
106
+ const projectId = projectIdOf(client);
107
+ if (!data.connectionId) {
108
+ return true;
109
+ }
110
+ if (data.kind === 'SUMMARY') {
111
+ const ids = typeof data.messageIds === 'string'
112
+ ? data.messageIds.split(',').map((id) => id.trim()).filter(Boolean)
113
+ : [];
114
+ if (ids.length === 0) {
115
+ // 메시지 ID 없는 묶음(구버전 서버) — 거를 기준이 없어 건수 그대로 낸다.
116
+ const count = Number.parseInt(data.count, 10);
117
+ client.emit('newMailSummary', {
118
+ projectId,
119
+ connectionId: data.connectionId,
120
+ count: Number.isFinite(count) ? count : null,
121
+ messageIds: [],
122
+ source: 'push'
123
+ });
124
+ return true;
125
+ }
126
+ const deduper = deduperOf(client);
127
+ const unseen = ids.filter((id) => deduper.accept(keyOf(projectId, data.connectionId, id)));
128
+ if (unseen.length > 0) {
129
+ client.emit('newMailSummary', {
130
+ projectId,
131
+ connectionId: data.connectionId,
132
+ count: unseen.length,
133
+ messageIds: unseen,
134
+ source: 'push'
135
+ });
136
+ }
137
+ return true;
138
+ }
139
+ if (!data.messageId || !deduperOf(client).accept(keyOf(projectId, data.connectionId, data.messageId))) {
140
+ return true;
141
+ }
142
+ client.emit('newMail', {
143
+ projectId,
144
+ connectionId: data.connectionId,
145
+ messageId: data.messageId,
146
+ threadId: data.threadId || null,
147
+ sender: (payload.notification && payload.notification.title) || data.title || null,
148
+ from: null,
149
+ subject: (payload.notification && payload.notification.body) || data.body || null,
150
+ receivedAt: null,
151
+ source: 'push'
152
+ });
153
+ return true;
154
+ }
@@ -3,6 +3,7 @@ import ConnectionManager from '../core/ConnectionManager.js';
3
3
  import ChatClient from '../chat/ChatClient.js';
4
4
  import WebRTCClient from '../webrtc/WebRTCClient.js';
5
5
  import PushManager from '../push/PushManager.js';
6
+ import { handleMailPush } from './mailNotifications.js';
6
7
 
7
8
  export function initializeSubClients(client) {
8
9
  if (client.connectionManager) {
@@ -127,6 +128,7 @@ export function applySessionMethods(TalkFlowClient) {
127
128
  if (this.chat) {
128
129
  this.chat.unsubscribeAllRooms();
129
130
  this.chat.unsubscribeRoomList();
131
+ this.chat.unsubscribeMail();
130
132
  }
131
133
 
132
134
  await this.connectionManager.disconnect();
@@ -217,6 +219,11 @@ export function applySessionMethods(TalkFlowClient) {
217
219
  return;
218
220
  }
219
221
 
222
+ // Gmail 메일 푸시 — 웹소켓 미리보기와 합쳐 newMail/newMailSummary 로만 낸다(pushNotification 아님).
223
+ if (handleMailPush(this, payload)) {
224
+ return;
225
+ }
226
+
220
227
  const activeRoomId = this.chat?.getActiveRoom();
221
228
 
222
229
  if (activeRoomId && activeRoomId === data.roomId) {
@@ -291,6 +298,10 @@ export function applySessionMethods(TalkFlowClient) {
291
298
  return PushManager.consumePendingRoom(this.options.projectId);
292
299
  };
293
300
 
301
+ TalkFlowClient.prototype.consumePendingMail = async function consumePendingMail() {
302
+ return PushManager.consumePendingMail(this.options.projectId);
303
+ };
304
+
294
305
  TalkFlowClient.prototype.setCurrentDeviceEnabled = function setCurrentDeviceEnabled(enabled) {
295
306
  return this.pushManager?.setCurrentDeviceEnabled(enabled);
296
307
  };
package/types/index.d.ts CHANGED
@@ -1340,6 +1340,71 @@ export interface RoomUnreadUpdatedEvent {
1340
1340
  unreadVersion: number;
1341
1341
  }
1342
1342
 
1343
+ /** Gmail 새 메일 미리보기 — 서버 `MAIL_CHANGED.messages[]` 원소. 본문은 없다. */
1344
+ export interface MailPreview {
1345
+ messageId: string;
1346
+ threadId?: string | null;
1347
+ from?: { name?: string | null; email: string } | null;
1348
+ subject?: string | null;
1349
+ /** ISO-8601 UTC. */
1350
+ receivedAt?: string | null;
1351
+ }
1352
+
1353
+ /**
1354
+ * `mailChanged` 이벤트 — 서버 `/user/queue/mail` 그대로. 받으면 열려 있는 메일 목록·안읽은 수를 다시 읽는다.
1355
+ * 읽음·보관 같은 변화면 `messages` 가 비어 있다(새 메일 알림을 띄우지 않는다).
1356
+ */
1357
+ export interface MailChangedEvent {
1358
+ eventType: 'MAIL_CHANGED';
1359
+ connectionId: string;
1360
+ /** 받은메일함 안읽은 수, 모르면 -1. */
1361
+ inboxUnread: number;
1362
+ newMessages: number;
1363
+ /** true 면 놓친 변화가 있을 수 있다 — 목록을 첫 쪽부터 다시 읽는다. */
1364
+ resync: boolean;
1365
+ timestamp: string;
1366
+ messages: MailPreview[];
1367
+ }
1368
+
1369
+ /**
1370
+ * `newMail` 이벤트 — 새 메일 한 통. 웹소켓 미리보기(`source: 'socket'`)와 포그라운드 FCM 메일 푸시(`source: 'push'`)를
1371
+ * `(projectId, connectionId, messageId)` 로 합쳐 메일마다 한 번만 온다.
1372
+ */
1373
+ export interface NewMailEvent {
1374
+ projectId: string | null;
1375
+ connectionId: string;
1376
+ messageId: string;
1377
+ threadId: string | null;
1378
+ /** 보낸 사람 표시(이름, 없으면 주소). */
1379
+ sender: string | null;
1380
+ /** 웹소켓으로 왔을 때만. */
1381
+ from: { name?: string | null; email: string } | null;
1382
+ subject: string | null;
1383
+ /** 웹소켓으로 왔을 때만(ISO-8601 UTC). */
1384
+ receivedAt: string | null;
1385
+ source: 'socket' | 'push';
1386
+ }
1387
+
1388
+ /**
1389
+ * `newMailSummary` 이벤트 — 한 번에 4통 이상 온 묶음 푸시(포그라운드). 받은메일함을 다시 읽는다.
1390
+ * 묶은 메일 중 아직 `newMail` 로 알리지 않은 것만 센다 — 전부 알린 메일이면 오지 않는다.
1391
+ */
1392
+ export interface NewMailSummaryEvent {
1393
+ projectId: string | null;
1394
+ connectionId: string;
1395
+ /** 처음 알리는 메일 수(구버전 서버의 묶음이면 서버가 준 건수). */
1396
+ count: number | null;
1397
+ /** 처음 알리는 메일 ID(구버전 서버의 묶음이면 빈 배열). */
1398
+ messageIds: string[];
1399
+ source: 'push';
1400
+ }
1401
+
1402
+ /** 알림 클릭으로 열린 앱이 꺼내는 메일 이동 — `messageId` 가 없으면(묶음 알림) 받은메일함으로. */
1403
+ export interface PendingMail {
1404
+ connectionId: string;
1405
+ messageId: string | null;
1406
+ }
1407
+
1343
1408
  /** 포그라운드 푸시 알림 payload. */
1344
1409
  export interface PushNotificationPayload {
1345
1410
  title: string;
@@ -1980,6 +2045,9 @@ export interface TalkFlowClientEvents {
1980
2045
  roomListRoomUpdated: RoomListUpdateEvent;
1981
2046
  /** 내가 어느 기기에서 읽든 방별 미독 절대값(+version)을 통지 — 크로스-디바이스 badge 동기화. */
1982
2047
  roomListUnreadUpdated: RoomUnreadUpdatedEvent;
2048
+ mailSubscribed: Record<string, unknown>;
2049
+ mailUnsubscribed: Record<string, unknown>;
2050
+ mailChanged: MailChangedEvent;
1983
2051
  retentionCleanup: RetentionCleanupPayload;
1984
2052
 
1985
2053
  // 갭 복구 — 복구된 메시지 자체는 일반 chatMessage 로 재생되므로 아래는 모두 선택 구독용.
@@ -2010,6 +2078,10 @@ export interface TalkFlowClientEvents {
2010
2078
  */
2011
2079
  pushFailed: PushFailedPayload;
2012
2080
  pushNotification: PushNotificationPayload;
2081
+ /** Gmail 새 메일 한 통 — 웹소켓·포그라운드 푸시를 합쳐 메일마다 한 번. 메일 푸시는 pushNotification 으로 오지 않는다. */
2082
+ newMail: NewMailEvent;
2083
+ /** Gmail 묶음 푸시(4통 이상) — 포그라운드. */
2084
+ newMailSummary: NewMailSummaryEvent;
2013
2085
 
2014
2086
  // WebRTC (re-emit from WebRTCClient)
2015
2087
  localStreamStarted: unknown;
@@ -2087,6 +2159,9 @@ export interface ChatClientEvents {
2087
2159
  roomListRoomUpdated: RoomListUpdateEvent;
2088
2160
  /** 내가 어느 기기에서 읽든 방별 미독 절대값(+version)을 통지 — 크로스-디바이스 badge 동기화. */
2089
2161
  roomListUnreadUpdated: RoomUnreadUpdatedEvent;
2162
+ mailSubscribed: Record<string, unknown>;
2163
+ mailUnsubscribed: Record<string, unknown>;
2164
+ mailChanged: MailChangedEvent;
2090
2165
  retentionCleanup: RetentionCleanupPayload;
2091
2166
  messageRead: MessageReadEventPayload;
2092
2167
  typing: TypingEvent;
@@ -2297,6 +2372,10 @@ export class ChatClient implements SdkEventEmitter<ChatClientEvents> {
2297
2372
  subscribeRoomList(): Promise<void>;
2298
2373
  unsubscribeRoomList(): void;
2299
2374
  isRoomListSubscribed(): boolean;
2375
+ /** Gmail 메일함 변화 구독(`/user/queue/mail`) — 자동 구독하지 않는다. */
2376
+ subscribeMail(): Promise<void>;
2377
+ unsubscribeMail(): void;
2378
+ isMailSubscribed(): boolean;
2300
2379
  getSubscribedRooms(): string[];
2301
2380
  isSubscribed(roomId: string): boolean;
2302
2381
 
@@ -2447,6 +2526,8 @@ export const PushErrorCode: {
2447
2526
 
2448
2527
  export class PushManager {
2449
2528
  consumePendingRoom(): Promise<string | null>;
2529
+ /** 알림 클릭(cold start)으로 저장된 Gmail 메일 이동을 꺼내고 지운다 — 10분 안의 것만. */
2530
+ consumePendingMail(): Promise<PendingMail | null>;
2450
2531
  reset(): void;
2451
2532
  getToken(): string | null;
2452
2533
  isEnabled(): boolean;
@@ -2457,6 +2538,7 @@ export class PushManager {
2457
2538
  /** 현재 디바이스의 푸시 수신 여부 토글. */
2458
2539
  setCurrentDeviceEnabled(enabled: boolean): Promise<void>;
2459
2540
  static consumePendingRoom(projectId?: string): Promise<string | null>;
2541
+ static consumePendingMail(projectId?: string): Promise<PendingMail | null>;
2460
2542
  /**
2461
2543
  * 현재 브라우저의 푸시 권한 상태. 인스턴스 없이도 호출 가능.
2462
2544
  */
@@ -2588,6 +2670,8 @@ export default class TalkFlowClient implements SdkEventEmitter<TalkFlowClientEve
2588
2670
  /** 푸시 상태 초기화 — 로그아웃 또는 사용자 전환 시 호출. logout() 내부에서 자동 호출됨. */
2589
2671
  resetPush(): void;
2590
2672
  consumePendingRoom(): Promise<string | null>;
2673
+ /** 알림 클릭(cold start)으로 저장된 Gmail 메일 이동을 꺼내고 지운다 — 앱 부팅 때 한 번. */
2674
+ consumePendingMail(): Promise<PendingMail | null>;
2591
2675
  /** 현재 디바이스의 푸시 수신 여부 토글. enable 선행 안 됐으면 undefined 반환. */
2592
2676
  setCurrentDeviceEnabled(enabled: boolean): Promise<void> | undefined;
2593
2677
  /** 특정 디바이스 토글 (관리 UI 용). enable 선행 안 됐으면 undefined 반환. */
@@ -2750,6 +2834,10 @@ export default class TalkFlowClient implements SdkEventEmitter<TalkFlowClientEve
2750
2834
  subscribeRoomList(): Promise<void>;
2751
2835
  unsubscribeRoomList(): void;
2752
2836
  isRoomListSubscribed(): boolean;
2837
+ /** Gmail 메일함 변화 구독(`/user/queue/mail`) — 자동 구독하지 않는다. */
2838
+ subscribeMail(): Promise<void>;
2839
+ unsubscribeMail(): void;
2840
+ isMailSubscribed(): boolean;
2753
2841
  getSubscribedRooms(): string[];
2754
2842
  isSubscribed(roomId: string): boolean;
2755
2843