@vibexnpm/talkx 3.2.1 → 4.0.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/index.d.ts +109 -30
- package/dist/talkflow-sdk.esm.js +1 -1
- package/dist/talkflow-sdk.esm.js.map +1 -1
- package/dist/talkflow-sdk.standalone.js +1 -1
- package/dist/talkflow-sdk.standalone.js.map +1 -1
- package/dist/talkflow-sdk.umd.js +1 -1
- package/dist/talkflow-sdk.umd.js.map +1 -1
- package/package.json +5 -3
- package/src/chat/ChatClient.js +62 -84
- package/src/constants.js +12 -0
- package/src/push/PushManager.js +13 -2
- package/src/talkflow/delegates.js +4 -0
- package/types/__typetests__/singleMessageAttachment.types.ts +68 -0
- package/types/__typetests__/tsconfig.json +12 -0
- package/types/index.d.ts +109 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibexnpm/talkx",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "TalkFlow SDK - Chat & WebRTC unified client",
|
|
6
6
|
"main": "dist/talkflow-sdk.umd.js",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"build:types": "copy types\\index.d.ts dist\\index.d.ts",
|
|
18
18
|
"dev": "rollup -c -w",
|
|
19
19
|
"lint": "eslint src/",
|
|
20
|
-
"test": "jest"
|
|
20
|
+
"test": "jest && npm run typecheck",
|
|
21
|
+
"typecheck": "tsc -p types/__typetests__/tsconfig.json"
|
|
21
22
|
},
|
|
22
23
|
"repository": {
|
|
23
24
|
"type": "git",
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"@rollup/plugin-terser": "^0.4.4",
|
|
49
50
|
"babel-jest": "^29.7.0",
|
|
50
51
|
"jest": "^29.7.0",
|
|
51
|
-
"rollup": "^4.12.0"
|
|
52
|
+
"rollup": "^4.12.0",
|
|
53
|
+
"typescript": "^5.9.3"
|
|
52
54
|
}
|
|
53
55
|
}
|
package/src/chat/ChatClient.js
CHANGED
|
@@ -1331,8 +1331,8 @@ class ChatClient extends EventEmitter {
|
|
|
1331
1331
|
* 설계된다. 서버 Unique 제약 + pre-check 로 중복 저장이 차단되며, 재시도 히트 시
|
|
1332
1332
|
* {@code duplicate: true} 로 기존 결과가 그대로 반환된다.</p>
|
|
1333
1333
|
*
|
|
1334
|
-
* <p
|
|
1335
|
-
*
|
|
1334
|
+
* <p><b>전송 1회 = 메시지 1건</b>: 첨부가 몇 개든 한 메시지로 저장되며 응답 {@code messages} 는 길이 1이다.
|
|
1335
|
+
* {@code chatMessageType} 은 렌더러 힌트로, 첨부가 동질이면 그 카테고리, 혼재면 {@code MIXED} 다.</p>
|
|
1336
1336
|
*
|
|
1337
1337
|
* <p>답글: {@code data.replyToMessageId} 에 원본 메시지의 서버 {@code messageId} 를 지정.
|
|
1338
1338
|
* 서버가 원본을 조회해서 snapshot 을 생성 후 저장하며, 응답/수신 메시지의 {@code replyTo}
|
|
@@ -1341,12 +1341,12 @@ class ChatClient extends EventEmitter {
|
|
|
1341
1341
|
* @param {string} roomId - 채팅방 ID
|
|
1342
1342
|
* @param {Object} data
|
|
1343
1343
|
* @param {string} [data.message] - 메시지 내용 (message 또는 fileInfos 중 하나 필수)
|
|
1344
|
-
* @param {Object[]} [data.fileInfos] - 파일 메타데이터
|
|
1345
|
-
*
|
|
1344
|
+
* @param {Object[]} [data.fileInfos] - 파일 메타데이터 배열. 각 원소는 <b>UUID {@code fileId} 필수</b>
|
|
1345
|
+
* ({@link #uploadFile} 반환값은 이미 포함). 외부 스토리지 직접 업로드 연동은 직접 채워야 한다.
|
|
1346
1346
|
* @param {string} [data.replyToMessageId] - 답글 대상 원본 메시지 ID (옵션)
|
|
1347
1347
|
* @returns {Promise<{duplicate: boolean, messages: Object[]}>}
|
|
1348
1348
|
* - {@code duplicate}: 멱등 재시도로 기존 결과를 반환한 경우 {@code true}
|
|
1349
|
-
* - {@code messages}: 저장된 메시지
|
|
1349
|
+
* - {@code messages}: 저장된 메시지 배열. {@code messageId} 는 서버가 부여한 ID.
|
|
1350
1350
|
*
|
|
1351
1351
|
* @example
|
|
1352
1352
|
* // 일반 메시지
|
|
@@ -1361,13 +1361,13 @@ class ChatClient extends EventEmitter {
|
|
|
1361
1361
|
* });
|
|
1362
1362
|
*
|
|
1363
1363
|
* @example
|
|
1364
|
-
* //
|
|
1364
|
+
* // 첨부 3장 + 캡션 — 메시지 1건
|
|
1365
1365
|
* const { messages } = await chat.sendMessage('room-id', {
|
|
1366
1366
|
* message: '사진 보냅니다',
|
|
1367
|
-
* fileInfos: [f0, f1, f2]
|
|
1368
|
-
* separateFiles: true
|
|
1367
|
+
* fileInfos: [f0, f1, f2] // uploadFile 결과 (fileId 포함)
|
|
1369
1368
|
* });
|
|
1370
|
-
* // messages.length ===
|
|
1369
|
+
* // messages.length === 1 · messages[0].chatMessageType === 'IMAGE'
|
|
1370
|
+
* // messages[0].fileMetaDataList.length === 3
|
|
1371
1371
|
*/
|
|
1372
1372
|
async sendMessage(roomId, data) {
|
|
1373
1373
|
const messageId = this._generateMessageId();
|
|
@@ -1382,12 +1382,12 @@ class ChatClient extends EventEmitter {
|
|
|
1382
1382
|
* 예측 {@code messageIds} 배열을 동기 반환한다. 호출자는 promise 가 resolve 되기 전에
|
|
1383
1383
|
* 미리 placeholder 메시지를 UI 에 그려둘 수 있고, resolve 후 같은 ID 로 안전하게 교체할 수 있다.</p>
|
|
1384
1384
|
*
|
|
1385
|
-
* <p
|
|
1386
|
-
*
|
|
1387
|
-
* {@code messages.map(m => m.messageId)} 와 길이/순서가 정확히 일치한다.</p>
|
|
1385
|
+
* <p>전송 1회 = 메시지 1건이므로 {@code messageIds} 는 <b>항상 {@code [baseMessageId]} 하나</b>다.
|
|
1386
|
+
* 서버 분류 규칙을 mirror 해 여러 ID 를 예측하던 로직은 제거됐다.</p>
|
|
1388
1387
|
*
|
|
1389
|
-
* <p><b
|
|
1390
|
-
*
|
|
1388
|
+
* <p><b>전환 기간 주의</b>: 서버가 아직 병합 저장을 켜지 않았다면 한 요청이 여러 row 로 저장되어
|
|
1389
|
+
* {@code optimistic.predictionMatched} 가 {@code false} 가 된다. 오류가 아니라 전환 기간의 정상
|
|
1390
|
+
* 상태이며, 이때는 응답의 {@code actualMessageIds} 로 재구성해야 한다.</p>
|
|
1391
1391
|
*
|
|
1392
1392
|
* @param {string} roomId - 채팅방 ID
|
|
1393
1393
|
* @param {Object} data - {@link #sendMessage} 와 동일한 페이로드
|
|
@@ -1407,7 +1407,8 @@ class ChatClient extends EventEmitter {
|
|
|
1407
1407
|
*/
|
|
1408
1408
|
sendMessageOptimistic(roomId, data) {
|
|
1409
1409
|
const baseMessageId = this._generateMessageId();
|
|
1410
|
-
|
|
1410
|
+
// 전송 1회 = 메시지 1건 — 예측할 것이 없다. 서버 분류 규칙을 mirror 하던 예측기는 제거됐다.
|
|
1411
|
+
const messageIds = [baseMessageId];
|
|
1411
1412
|
|
|
1412
1413
|
const promise = this._sendMessageWithId(roomId, baseMessageId, data)
|
|
1413
1414
|
.then((result) => this._attachOptimisticMetadata(messageIds, result));
|
|
@@ -1430,13 +1431,14 @@ class ChatClient extends EventEmitter {
|
|
|
1430
1431
|
// 메시지 전송 시 타이핑 표시 자동 중단
|
|
1431
1432
|
this.stopTyping(roomId);
|
|
1432
1433
|
|
|
1433
|
-
// separateFiles 는
|
|
1434
|
-
//
|
|
1434
|
+
// separateFiles 는 더 이상 보내지 않는다 — 전송 1회 = 메시지 1건이 계약이다.
|
|
1435
|
+
// 서버는 이 필드를 병합 저장 플래그가 OFF 인 동안에만(legacy 재현용) 읽으며, 값이 없으면
|
|
1436
|
+
// 기본 true 로 동작한다. 즉 OFF 서버에서는 여전히 N건이 저장되고, 그 불일치는
|
|
1437
|
+
// optimistic 의 predictionMatched:false 로 드러난다(설계 §5).
|
|
1435
1438
|
const response = await this.apiClient.post(`/api/v1/chat-messages/${roomId}/send`, {
|
|
1436
1439
|
messageId,
|
|
1437
1440
|
message: data.message,
|
|
1438
1441
|
fileInfos: data.fileInfos,
|
|
1439
|
-
separateFiles: data.separateFiles !== false,
|
|
1440
1442
|
replyToMessageId: data.replyToMessageId
|
|
1441
1443
|
});
|
|
1442
1444
|
|
|
@@ -1542,7 +1544,6 @@ class ChatClient extends EventEmitter {
|
|
|
1542
1544
|
* @param {File|Blob|Array<File|Blob>} files - 단일 파일 또는 파일 배열
|
|
1543
1545
|
* @param {Object} [options]
|
|
1544
1546
|
* @param {string} [options.message] - 함께 보낼 텍스트 본문
|
|
1545
|
-
* @param {boolean} [options.separateFiles] - 파일별 개별 메시지 분리 여부 (서버 설정)
|
|
1546
1547
|
* @param {string} [options.replyToMessageId] - 답글 대상 원본 메시지의 서버 {@code messageId}
|
|
1547
1548
|
* @param {Function} [options.onUploadProgress] - {@code ({loaded,total,percent,fileIndex}) => void} — 파일별 진행률
|
|
1548
1549
|
* @param {AbortSignal} [options.signal] - 취소 시그널 — 업로드 중에만 유효
|
|
@@ -1571,7 +1572,6 @@ class ChatClient extends EventEmitter {
|
|
|
1571
1572
|
return this.sendMessage(roomId, {
|
|
1572
1573
|
message: options.message,
|
|
1573
1574
|
fileInfos,
|
|
1574
|
-
separateFiles: options.separateFiles,
|
|
1575
1575
|
replyToMessageId: options.replyToMessageId
|
|
1576
1576
|
});
|
|
1577
1577
|
}
|
|
@@ -1616,11 +1616,7 @@ class ChatClient extends EventEmitter {
|
|
|
1616
1616
|
const fileArray = this._normalizeFileArray(files);
|
|
1617
1617
|
|
|
1618
1618
|
const baseMessageId = this._generateMessageId();
|
|
1619
|
-
const messageIds =
|
|
1620
|
-
baseMessageId,
|
|
1621
|
-
this._predictGroupCount(fileArray, options.separateFiles !== false),
|
|
1622
|
-
this._hasTextMessage(options.message)
|
|
1623
|
-
);
|
|
1619
|
+
const messageIds = [baseMessageId]; // 전송 1회 = 메시지 1건
|
|
1624
1620
|
|
|
1625
1621
|
const promise = (async () => {
|
|
1626
1622
|
const metas = await this._uploadFiles(roomId, fileArray, options);
|
|
@@ -1628,7 +1624,6 @@ class ChatClient extends EventEmitter {
|
|
|
1628
1624
|
const result = await this._sendMessageWithId(roomId, baseMessageId, {
|
|
1629
1625
|
message: options.message,
|
|
1630
1626
|
fileInfos,
|
|
1631
|
-
separateFiles: options.separateFiles,
|
|
1632
1627
|
replyToMessageId: options.replyToMessageId
|
|
1633
1628
|
});
|
|
1634
1629
|
return this._attachOptimisticMetadata(messageIds, result);
|
|
@@ -1811,20 +1806,23 @@ class ChatClient extends EventEmitter {
|
|
|
1811
1806
|
}
|
|
1812
1807
|
|
|
1813
1808
|
/**
|
|
1814
|
-
* MIME 타입 →
|
|
1815
|
-
*
|
|
1809
|
+
* MIME 타입 → {@code ChatMessageType} 카테고리 (서버 {@code AttachmentTypeClassifier} mirror).
|
|
1810
|
+
*
|
|
1811
|
+
* <p><b>공개 API</b> — legacy {@code GALLERY} 메시지의 실제 카테고리를 복원하는 데 쓴다
|
|
1812
|
+
* ({@link #resolveMessageCategory}). 서버 규칙이 바뀌면 본 메서드도 동기화해야 한다.</p>
|
|
1816
1813
|
*
|
|
1817
|
-
* @private
|
|
1818
1814
|
* @param {string} mimeType
|
|
1819
|
-
* @returns {
|
|
1815
|
+
* @returns {'IMAGE'|'VIDEO'|'AUDIO'|'DOCUMENT'|'FILE'}
|
|
1820
1816
|
*/
|
|
1821
|
-
|
|
1817
|
+
classifyFileType(mimeType) {
|
|
1822
1818
|
const mime = this._baseMimeType(mimeType);
|
|
1823
1819
|
if (mime.startsWith('image/')) return 'IMAGE';
|
|
1824
1820
|
if (mime.startsWith('video/')) return 'VIDEO';
|
|
1825
1821
|
if (mime.startsWith('audio/')) return 'AUDIO';
|
|
1826
|
-
// 평문(txt/md)도 DOCUMENT — 서버가 온디맨드 번역·PM 문서 인식 대상으로 삼는 기준과 동일해야
|
|
1822
|
+
// 평문(txt/md)도 DOCUMENT — 서버가 온디맨드 번역·PM 문서 인식 대상으로 삼는 기준과 동일해야 한다.
|
|
1823
|
+
// pre-2007 Office(.doc/.xls/.ppt)도 서버가 OLE 매직까지 검증해 DOCUMENT 로 받는다.
|
|
1827
1824
|
if (mime.includes('pdf') || mime.includes('document')
|
|
1825
|
+
|| mime.includes('msword') || mime.includes('ms-excel') || mime.includes('ms-powerpoint')
|
|
1828
1826
|
|| PLAIN_TEXT_DOCUMENT_MIME_TYPES.has(mime)) {
|
|
1829
1827
|
return 'DOCUMENT';
|
|
1830
1828
|
}
|
|
@@ -1832,66 +1830,46 @@ class ChatClient extends EventEmitter {
|
|
|
1832
1830
|
}
|
|
1833
1831
|
|
|
1834
1832
|
/**
|
|
1835
|
-
*
|
|
1836
|
-
* <p>{@code separateFiles=true} 면 파일 개수, {@code false} 면 타입별 distinct 개수.
|
|
1837
|
-
* File 객체 ({@code .type}) / FileMetaData 객체 ({@code .fileType}) 양쪽 입력 모두 처리.</p>
|
|
1838
|
-
*
|
|
1833
|
+
* 하위호환 별칭 — {@link #classifyFileType} 를 쓸 것.
|
|
1839
1834
|
* @private
|
|
1840
|
-
* @
|
|
1841
|
-
* @param {boolean} separateFiles
|
|
1842
|
-
* @returns {number}
|
|
1835
|
+
* @deprecated
|
|
1843
1836
|
*/
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
if (separateFiles) return fileArray.length;
|
|
1847
|
-
|
|
1848
|
-
// 입력 순서 보존하면서 distinct 타입 카운트 (서버 LinkedHashMap 동작과 동일)
|
|
1849
|
-
const typesSeen = new Set();
|
|
1850
|
-
for (const file of fileArray) {
|
|
1851
|
-
const mime = file && (file.type || file.fileType) || '';
|
|
1852
|
-
typesSeen.add(this._classifyFileType(mime));
|
|
1853
|
-
}
|
|
1854
|
-
return typesSeen.size;
|
|
1837
|
+
_classifyFileType(mimeType) {
|
|
1838
|
+
return this.classifyFileType(mimeType);
|
|
1855
1839
|
}
|
|
1856
1840
|
|
|
1857
1841
|
/**
|
|
1858
|
-
*
|
|
1859
|
-
* <p>규칙 ({@code SendMessageService.java:102-106, 197}):
|
|
1860
|
-
* {@code expectedMessageCount = (hasMessage?1:0) + groupCount}, 1 이면 base, 2 이상이면
|
|
1861
|
-
* 텍스트=base / 파일=base#0..#(N-1).</p>
|
|
1842
|
+
* 메시지의 <b>렌더 카테고리</b>를 해석한다 — legacy {@code GALLERY} 복원용 공개 헬퍼.
|
|
1862
1843
|
*
|
|
1863
|
-
* @
|
|
1864
|
-
* @
|
|
1865
|
-
* @
|
|
1866
|
-
* @
|
|
1867
|
-
* @returns {string[]} 응답 messages 와 같은 순서
|
|
1868
|
-
*/
|
|
1869
|
-
_predictMessageIds(baseMessageId, groupCount, hasMessage) {
|
|
1870
|
-
const expectedCount = (hasMessage ? 1 : 0) + groupCount;
|
|
1871
|
-
if (expectedCount === 0) return [];
|
|
1872
|
-
if (expectedCount === 1) return [baseMessageId];
|
|
1873
|
-
|
|
1874
|
-
const ids = [];
|
|
1875
|
-
if (hasMessage) ids.push(baseMessageId);
|
|
1876
|
-
for (let i = 0; i < groupCount; i++) {
|
|
1877
|
-
ids.push(`${baseMessageId}#${i}`);
|
|
1878
|
-
}
|
|
1879
|
-
return ids;
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
/**
|
|
1883
|
-
* {@code SendMessageData} 페이로드로부터 messageIds 예측.
|
|
1884
|
-
* <p>{@link #sendMessageOptimistic} 가 사용 — {@code data.fileInfos} 가 있으면 그걸로
|
|
1885
|
-
* group count 예측, 없으면 텍스트 단건으로 처리.</p>
|
|
1844
|
+
* <p>서버는 이제 첨부가 동질이면 그 카테고리를, 혼재면 {@code MIXED} 를 보낸다. 그러나 이 변경
|
|
1845
|
+
* <b>이전에 저장된 메시지는 복수 첨부면 타입 무관 전부 {@code GALLERY}</b> 다 — 동영상 2개도, PDF 2개도
|
|
1846
|
+
* {@code GALLERY} 다. 따라서 <b>{@code GALLERY} 를 {@code IMAGE} 로 매핑하면 안 된다.</b>
|
|
1847
|
+
* 이 메서드가 {@code fileMetaDataList} 의 MIME 으로 실제 카테고리를 복원한다.</p>
|
|
1886
1848
|
*
|
|
1887
|
-
* @
|
|
1849
|
+
* @param {Object} message - 서버 메시지 ({@code chatMessageType} + {@code fileMetaDataList})
|
|
1850
|
+
* @returns {string} 'TEXT'|'IMAGE'|'VIDEO'|'AUDIO'|'DOCUMENT'|'FILE'|'MIXED'|'SYSTEM'
|
|
1851
|
+
*
|
|
1852
|
+
* @example
|
|
1853
|
+
* // 렌더러 선택 — GALLERY 분기를 따로 두지 않아도 된다
|
|
1854
|
+
* switch (client.resolveMessageCategory(message)) {
|
|
1855
|
+
* case 'IMAGE': return <ImageGrid files={message.fileMetaDataList} />;
|
|
1856
|
+
* case 'VIDEO': return <VideoList files={message.fileMetaDataList} />;
|
|
1857
|
+
* case 'MIXED': return <MixedAttachmentList files={message.fileMetaDataList} />;
|
|
1858
|
+
* default: return <FileList files={message.fileMetaDataList} />;
|
|
1859
|
+
* }
|
|
1888
1860
|
*/
|
|
1889
|
-
|
|
1890
|
-
const
|
|
1891
|
-
const
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1861
|
+
resolveMessageCategory(message) {
|
|
1862
|
+
const type = message && message.chatMessageType;
|
|
1863
|
+
const files = (message && message.fileMetaDataList) || [];
|
|
1864
|
+
// GALLERY 만 복원 대상 — 나머지는 서버가 이미 정확한 카테고리를 보냈다.
|
|
1865
|
+
if (type !== 'GALLERY') {
|
|
1866
|
+
return type || 'TEXT';
|
|
1867
|
+
}
|
|
1868
|
+
if (files.length === 0) {
|
|
1869
|
+
return 'FILE';
|
|
1870
|
+
}
|
|
1871
|
+
const categories = new Set(files.map(f => this.classifyFileType((f && f.fileType) || '')));
|
|
1872
|
+
return categories.size === 1 ? categories.values().next().value : 'MIXED';
|
|
1895
1873
|
}
|
|
1896
1874
|
|
|
1897
1875
|
/**
|
package/src/constants.js
CHANGED
|
@@ -95,6 +95,18 @@ export const ChatMessageType = {
|
|
|
95
95
|
FILE: 'FILE',
|
|
96
96
|
VIDEO: 'VIDEO',
|
|
97
97
|
AUDIO: 'AUDIO',
|
|
98
|
+
DOCUMENT: 'DOCUMENT',
|
|
99
|
+
/**
|
|
100
|
+
* 서로 다른 카테고리의 첨부가 섞인 메시지 — 예: 이미지 2 + PDF 1.
|
|
101
|
+
* <p>전송 1회 = 메시지 1건 계약에서 새로 생긴 값. 클라이언트가 모르면 렌더가 깨진다.</p>
|
|
102
|
+
*/
|
|
103
|
+
MIXED: 'MIXED',
|
|
104
|
+
/**
|
|
105
|
+
* @deprecated 복수 첨부(타입 무관)를 뜻하던 <b>과거</b> 값. 신규 저장에서는 생성되지 않지만
|
|
106
|
+
* 기존 데이터에 남아 있다. 동영상·문서 복수도 전부 이 값이므로 <b>IMAGE 로 매핑하면 안 되고</b>,
|
|
107
|
+
* {@code ChatClient#resolveMessageCategory} 로 fileMetaDataList 의 MIME 에서 복원해야 한다.
|
|
108
|
+
*/
|
|
109
|
+
GALLERY: 'GALLERY',
|
|
98
110
|
SYSTEM: 'SYSTEM'
|
|
99
111
|
};
|
|
100
112
|
|
package/src/push/PushManager.js
CHANGED
|
@@ -411,9 +411,20 @@ class PushManager {
|
|
|
411
411
|
|
|
412
412
|
// 알림 권한 요청
|
|
413
413
|
// - 'default' 상태: 브라우저가 권한창을 표시하고 사용자 응답을 대기
|
|
414
|
-
// - 'granted' 상태: 즉시
|
|
414
|
+
// - 'granted' 상태: 재요청하지 않고 즉시 통과 (아래 주의사항 참고)
|
|
415
415
|
// - 'denied' 상태: 위 가드에서 이미 throw 했으므로 여기 도달 불가
|
|
416
|
-
|
|
416
|
+
//
|
|
417
|
+
// !! Safari/WebKit 주의 !!
|
|
418
|
+
// Chromium 은 이미 granted 인 상태에서 requestPermission() 을 호출하면 창 없이 즉시
|
|
419
|
+
// 'granted' 를 반환하지만, Safari/WebKit 은 이 API 를 '사용자 제스처 안에서만' 허용한다.
|
|
420
|
+
// 따라서 연결 직후 자동 활성화처럼 제스처가 없는 경로에서는 이미 허용된 상태여도 거부로 반환되어
|
|
421
|
+
// PERMISSION_DENIED 로 throw 되고, 그 아래의 토큰 발급/서버 등록/포그라운드 onMessage 등록까지
|
|
422
|
+
// 모두 건너뛰게 된다(iOS 홈 화면 앱에서 새로고침마다 권한 차단 배너 + 포그라운드 메시지 미수신).
|
|
423
|
+
// 이미 허용된 상태에서는 재요청 자체가 불필요하므로 호출을 건너뛴다.
|
|
424
|
+
const permission =
|
|
425
|
+
Notification.permission === 'granted'
|
|
426
|
+
? 'granted'
|
|
427
|
+
: await Notification.requestPermission();
|
|
417
428
|
if (permission !== 'granted') {
|
|
418
429
|
throw new PushError(
|
|
419
430
|
PushErrorCode.PERMISSION_DENIED,
|
|
@@ -27,6 +27,10 @@ const CHAT_DELEGATE_METHODS = [
|
|
|
27
27
|
'uploadFile',
|
|
28
28
|
'sendFileMessage',
|
|
29
29
|
'sendFileMessageOptimistic',
|
|
30
|
+
// 첨부 분류 — legacy GALLERY 복원용 공개 헬퍼. 타입 선언이 최상위 클라이언트에도 있으므로
|
|
31
|
+
// 여기에 없으면 talkflow.resolveMessageCategory() 가 런타임 undefined 가 된다(타입이 거짓말).
|
|
32
|
+
'classifyFileType',
|
|
33
|
+
'resolveMessageCategory',
|
|
30
34
|
'editMessage',
|
|
31
35
|
'deleteMessage',
|
|
32
36
|
'markAsRead',
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript 소비자 컴파일 검증 — 타입 선언이 <b>실제로 컴파일되는지</b>.
|
|
3
|
+
*
|
|
4
|
+
* <p>런타임 테스트(jest)는 `.d.ts` 를 전혀 보지 않으므로, 존재하지 않는 타입을 반환 타입으로 쓰거나
|
|
5
|
+
* 값(const)을 타입 위치에 쓰는 오류를 못 잡는다. S2 에서 실제로 `resolveMessageCategory` 가
|
|
6
|
+
* 값인 `ChatMessageType` 을 반환 타입으로 선언해 소비자 컴파일이 깨졌다.</p>
|
|
7
|
+
*
|
|
8
|
+
* <p>실행: {@code npm run typecheck}</p>
|
|
9
|
+
*/
|
|
10
|
+
import type TalkFlowClient from '../index';
|
|
11
|
+
import type {
|
|
12
|
+
ChatClient,
|
|
13
|
+
ChatMessage,
|
|
14
|
+
ChatMessageTypeValue,
|
|
15
|
+
FileMetaData,
|
|
16
|
+
SendFileInfo
|
|
17
|
+
} from '../index';
|
|
18
|
+
import { ChatMessageType } from '../index';
|
|
19
|
+
|
|
20
|
+
declare const chat: ChatClient;
|
|
21
|
+
declare const talkflow: TalkFlowClient;
|
|
22
|
+
declare const message: ChatMessage;
|
|
23
|
+
|
|
24
|
+
// 1) 렌더 카테고리 해석 — 반환은 값이 아니라 union 타입이어야 한다.
|
|
25
|
+
const category: ChatMessageTypeValue = chat.resolveMessageCategory(message);
|
|
26
|
+
const viaTopLevel: ChatMessageTypeValue = talkflow.resolveMessageCategory(message);
|
|
27
|
+
|
|
28
|
+
// 2) 신규 상수가 런타임 선언에 있어야 한다.
|
|
29
|
+
const mixed: 'MIXED' = ChatMessageType.MIXED;
|
|
30
|
+
const gallery: 'GALLERY' = ChatMessageType.GALLERY;
|
|
31
|
+
const document: 'DOCUMENT' = ChatMessageType.DOCUMENT;
|
|
32
|
+
|
|
33
|
+
// 3) MIXED 를 포함한 exhaustive 분기가 성립해야 한다.
|
|
34
|
+
function render(c: ChatMessageTypeValue): string {
|
|
35
|
+
switch (c) {
|
|
36
|
+
case 'TEXT': return 'text';
|
|
37
|
+
case 'IMAGE': return 'image';
|
|
38
|
+
case 'VIDEO': return 'video';
|
|
39
|
+
case 'AUDIO': return 'audio';
|
|
40
|
+
case 'DOCUMENT': return 'document';
|
|
41
|
+
case 'FILE': return 'file';
|
|
42
|
+
case 'MIXED': return 'mixed';
|
|
43
|
+
case 'GALLERY': return 'legacy';
|
|
44
|
+
case 'SYSTEM': return 'system';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 4) uploadFile 결과는 fileId 를 보장하므로 그대로 전송에 넘길 수 있다.
|
|
49
|
+
async function uploadThenSend(): Promise<void> {
|
|
50
|
+
const meta: SendFileInfo = await chat.uploadFile('room', new File([], 'a.png'));
|
|
51
|
+
await chat.sendMessage('room', { message: '캡션', fileInfos: [meta] });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 5) optimistic 핸들 — messageIds 는 문자열 배열.
|
|
55
|
+
function optimistic(): void {
|
|
56
|
+
const { baseMessageId, messageIds, promise } = chat.sendMessageOptimistic('room', { message: 'hi' });
|
|
57
|
+
void baseMessageId;
|
|
58
|
+
const ids: string[] = messageIds;
|
|
59
|
+
void ids;
|
|
60
|
+
void promise;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 6) 수신 메시지의 FileMetaData 는 legacy 를 위해 fileId 가 optional 이다.
|
|
64
|
+
function legacyResponseFile(f: FileMetaData): string | undefined {
|
|
65
|
+
return f.fileId;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { category, viaTopLevel, mixed, gallery, document, render, uploadThenSend, optimistic, legacyResponseFile };
|