Skip to content

Commit 1699d58

Browse files
authored
feat: api to add attachments for mms (#743)
* feat: support to add attachments for mms * feat: api to send mms * misc: use attachment.content * feat: limit attachment mime
1 parent 2401b09 commit 1699d58

File tree

4 files changed

+156
-54
lines changed

4 files changed

+156
-54
lines changed

docs/integration/api.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,17 +279,26 @@ document.querySelector("#rc-widget-adapter-frame").contentWindow.postMessage({
279279
type: 'rc-adapter-new-sms',
280280
phoneNumber: `phone number`,
281281
text: `your text`,
282+
// attachments: [{
283+
// name: 'test.txt',
284+
// content: 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D', // base64 encoded data URI
285+
// }], // optional for sending MMS message with attachments
286+
}]
282287
}, '*');
283288
```
284289

285-
If you are using Adapter JS way, just you can just call `RCAdapter.clickToSMS('phonenumber')`.
290+
If you are using Adapter JS way, just you can just call `RCAdapter.clickToSMS('phonenumber', 'text')`.
286291

287292
### Auto-populate SMS conversation text
288293

289294
```js
290295
document.querySelector("#rc-widget-adapter-frame").contentWindow.postMessage({
291296
type: 'rc-adapter-auto-populate-conversation',
292297
text: `your text`,
298+
// attachments: [{
299+
// name: 'test.png',
300+
// content: 'data:image/png;base64,SGVsbG8sIFdvcmxkIQ%3D%3D', // base64 encoded data URI. Limitation: https://developers.ringcentral.com/guide/messaging/sms/sending-images
301+
// }], // optional for sending MMS message with attachments, supported from v1.10.0.
293302
}, '*');
294303
```
295304

src/lib/Adapter/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,13 +515,14 @@ class Adapter extends AdapterCore {
515515
});
516516
}
517517

518-
clickToSMS(phoneNumber, text, conversation) {
518+
clickToSMS(phoneNumber, text, conversation, attachments = undefined) {
519519
this.setMinimized(false);
520520
this._postMessage({
521521
type: 'rc-adapter-new-sms',
522522
phoneNumber,
523523
text,
524524
conversation,
525+
attachments,
525526
});
526527
}
527528

src/modules/Adapter/helper.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import {
2+
messageIsTextMessage,
3+
} from '@ringcentral-integration/commons/lib/messageHelper';
4+
5+
export function findExistedConversation(conversations, phoneNumber) {
6+
return conversations.find((conversation) => {
7+
if (!conversation.to || conversation.to.length > 1) {
8+
return false;
9+
}
10+
if (!messageIsTextMessage(conversation)) {
11+
return false;
12+
}
13+
if (conversation.direction === 'Inbound') {
14+
return conversation.from && (
15+
conversation.from.phoneNumber === phoneNumber ||
16+
conversation.from.extensionNumber === phoneNumber
17+
);
18+
}
19+
return conversation.to.find(
20+
number => (
21+
number.phoneNumber === phoneNumber ||
22+
number.extensionNumber === phoneNumber
23+
)
24+
);
25+
});
26+
}
27+
28+
export function setOutputDeviceWhenCall(webphone, audioSettings) {
29+
if (webphone._webphone) {
30+
if (webphone._remoteVideo && webphone._remoteVideo.setSinkId) {
31+
if (audioSettings.outputDeviceId === 'default') {
32+
const defaultDevice = audioSettings.outputDevice;
33+
const defaultDeviceLabel = defaultDevice.label;
34+
const deviceLabel = defaultDeviceLabel.split(' - ')[1];
35+
if (deviceLabel) {
36+
const device = audioSettings.availableOutputDevices.find(
37+
(device) => device.label === deviceLabel
38+
);
39+
if (device) {
40+
webphone._remoteVideo.setSinkId(device.deviceId);
41+
}
42+
}
43+
} else {
44+
webphone._remoteVideo.setSinkId(audioSettings.outputDeviceId);
45+
}
46+
}
47+
}
48+
}
49+
50+
const SUPPORTED_MIME = [
51+
'image/jpeg',
52+
'image/png',
53+
'image/bmp',
54+
'image/gif',
55+
'image/tiff',
56+
'image/svg+xml',
57+
'video/3gpp',
58+
'video/mp4',
59+
'video/mpeg',
60+
'video/msvideo',
61+
'audio/mpeg',
62+
'text/vcard',
63+
'application/zip',
64+
'application/gzip',
65+
'application/rtf'
66+
];
67+
68+
function dataURLtoBlob(dataUrl) {
69+
const arr = dataUrl.split(',');
70+
const mime = arr[0].match(/:(.*?);/)[1];
71+
if (!SUPPORTED_MIME.includes(mime)) {
72+
console.warn('Unsupported mime type:', mime);
73+
return null;
74+
}
75+
const bStr = atob(arr[1]);
76+
let n = bStr.length;
77+
const u8arr = new Uint8Array(n);
78+
while (n--) {
79+
u8arr[n] = bStr.charCodeAt(n);
80+
}
81+
return new Blob([u8arr], { type: mime });
82+
}
83+
84+
export function getValidAttachments(attachments = []) {
85+
if (!attachments || !attachments.length) {
86+
return [];
87+
}
88+
const validAttachments = [];
89+
attachments.forEach((attachment) => {
90+
if (!attachment || !attachment.name || !attachment.content) {
91+
console.warn('Invalid attachment:', attachment);
92+
return;
93+
}
94+
if (typeof attachment.name !== 'string') {
95+
console.warn('Invalid attachment name:', attachment.name);
96+
return;
97+
}
98+
// only support base64 url
99+
if (
100+
typeof attachment.content !== 'string' ||
101+
attachment.content.indexOf('data:') !== 0 ||
102+
attachment.content.indexOf(';base64,') === -1
103+
) {
104+
console.warn('Invalid attachment content:', attachment.content);
105+
return;
106+
}
107+
const blob = dataURLtoBlob(attachment.content);
108+
if (!blob) {
109+
return;
110+
}
111+
validAttachments.push({
112+
name: attachment.name,
113+
file: blob,
114+
size: blob.size,
115+
});
116+
});
117+
return validAttachments;
118+
}

src/modules/Adapter/index.js

Lines changed: 26 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ import {
66
import debounce from '@ringcentral-integration/commons/lib/debounce';
77
import { Module } from '@ringcentral-integration/commons/lib/di';
88
import ensureExist from '@ringcentral-integration/commons/lib/ensureExist';
9-
import {
10-
messageIsTextMessage,
11-
} from '@ringcentral-integration/commons/lib/messageHelper';
129
import normalizeNumber
1310
from '@ringcentral-integration/commons/lib/normalizeNumber';
1411
import {
@@ -42,51 +39,11 @@ import {
4239
import PopupWindowManager from '../../lib/PopupWindowManager';
4340
import actionTypes from './actionTypes';
4441
import getReducer from './getReducer';
45-
46-
function findExistedConversation(conversations, phoneNumber) {
47-
return conversations.find((conversation) => {
48-
if (!conversation.to || conversation.to.length > 1) {
49-
return false;
50-
}
51-
if (!messageIsTextMessage(conversation)) {
52-
return false;
53-
}
54-
if (conversation.direction === 'Inbound') {
55-
return conversation.from && (
56-
conversation.from.phoneNumber === phoneNumber ||
57-
conversation.from.extensionNumber === phoneNumber
58-
);
59-
}
60-
return conversation.to.find(
61-
number => (
62-
number.phoneNumber === phoneNumber ||
63-
number.extensionNumber === phoneNumber
64-
)
65-
);
66-
});
67-
}
68-
69-
function setOutputDeviceWhenCall(webphone, audioSettings) {
70-
if (webphone._webphone) {
71-
if (webphone._remoteVideo && webphone._remoteVideo.setSinkId) {
72-
if (audioSettings.outputDeviceId === 'default') {
73-
const defaultDevice = audioSettings.outputDevice;
74-
const defaultDeviceLabel = defaultDevice.label;
75-
const deviceLabel = defaultDeviceLabel.split(' - ')[1];
76-
if (deviceLabel) {
77-
const device = audioSettings.availableOutputDevices.find(
78-
(device) => device.label === deviceLabel
79-
);
80-
if (device) {
81-
webphone._remoteVideo.setSinkId(device.deviceId);
82-
}
83-
}
84-
} else {
85-
webphone._remoteVideo.setSinkId(audioSettings.outputDeviceId);
86-
}
87-
}
88-
}
89-
}
42+
import {
43+
findExistedConversation,
44+
setOutputDeviceWhenCall,
45+
getValidAttachments,
46+
} from './helper';
9047

9148
@Module({
9249
name: 'Adapter',
@@ -306,13 +263,13 @@ export default class Adapter extends AdapterModuleCore {
306263
}
307264
break;
308265
case 'rc-adapter-new-sms':
309-
this._newSMS(data.phoneNumber, data.text, data.conversation);
266+
this._newSMS(data.phoneNumber, data.text, data.conversation, data.attachments);
310267
break;
311268
case 'rc-adapter-new-call':
312269
this._newCall(data.phoneNumber, data.toCall);
313270
break;
314271
case 'rc-adapter-auto-populate-conversation':
315-
this._autoPopulateConversationText(data.text);
272+
this._autoPopulateConversationText(data.text, data.attachments);
316273
break;
317274
case 'rc-adapter-control-call':
318275
this._controlCall(data.callAction, data.callId, data.options);
@@ -897,7 +854,7 @@ export default class Adapter extends AdapterModuleCore {
897854
}
898855
}
899856

900-
_newSMS(phoneNumber, text, conversation) {
857+
_newSMS(phoneNumber, text, conversation, attachments = null) {
901858
if (!this._auth.loggedIn) {
902859
return;
903860
}
@@ -913,12 +870,18 @@ export default class Adapter extends AdapterModuleCore {
913870
normalizedNumber,
914871
);
915872
}
873+
const validAttachments = getValidAttachments(attachments);
916874
if (existedConversation) {
917875
this._router.push(`/conversations/${existedConversation.conversationId}`);
918876
if (text && text.length > 0) {
919877
this._conversations.loadConversation(existedConversation.conversationId);
920878
this._conversations.updateMessageText(String(text));
921879
}
880+
if (validAttachments.length > 0) {
881+
validAttachments.forEach((attachment) => {
882+
this._conversations.addAttachment(attachment);
883+
})
884+
}
922885
return;
923886
}
924887
this._router.push('/composeText');
@@ -928,6 +891,11 @@ export default class Adapter extends AdapterModuleCore {
928891
if (text && text.length > 0) {
929892
this._composeText.updateMessageText(String(text));
930893
}
894+
if (validAttachments.length > 0) {
895+
validAttachments.forEach((attachment) => {
896+
this._composeText.addAttachment(attachment);
897+
});
898+
}
931899
}
932900

933901
_newCall(phoneNumber, toCall = false) {
@@ -948,13 +916,19 @@ export default class Adapter extends AdapterModuleCore {
948916
}
949917
}
950918

951-
_autoPopulateConversationText(text) {
919+
_autoPopulateConversationText(text, attachments) {
952920
if (!this._conversations.currentConversationId) {
953921
return;
954922
}
955923
if (typeof text === 'string') {
956924
this._conversations.updateMessageText(text);
957925
}
926+
const validAttachments = getValidAttachments(attachments);
927+
if (validAttachments.length > 0) {
928+
validAttachments.forEach((attachment) => {
929+
this._conversations.addAttachment(attachment);
930+
});
931+
}
958932
}
959933

960934
_isCallOngoing(phoneNumber) {

0 commit comments

Comments
 (0)