Compare commits
20
Commits
0598a42164
...
e42a7ca6de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e42a7ca6de | ||
|
|
609d3e3a9f | ||
|
|
038f62d535 | ||
|
|
37451b9aff | ||
|
|
00a1b656e4 | ||
|
|
e0c412a33c | ||
|
|
a2ca095c24 | ||
|
|
42ae953055 | ||
|
|
9c2d3f3693 | ||
|
|
9d8a1fdfc4 | ||
|
|
c6a9df9fe9 | ||
|
|
470d38e583 | ||
|
|
65dff7d168 | ||
|
|
d2d83a0865 | ||
|
|
aa498a7d1f | ||
|
|
d106f6382a | ||
|
|
cdff2ee697 | ||
|
|
d76756c67a | ||
|
|
4edb48fd24 | ||
|
|
860ae42513 |
@@ -44,7 +44,7 @@ More translations are welcome. See [instructions](https://github.com/tinode/chat
|
|||||||
|
|
||||||
## Not done yet
|
## Not done yet
|
||||||
|
|
||||||
* Voice or video messages, video or audio calling.
|
* Video or audio calling.
|
||||||
* End-to-End encryption.
|
* End-to-End encryption.
|
||||||
* Emoji support is weak.
|
* Emoji support is weak.
|
||||||
* Mentions, hashtags.
|
* Mentions, hashtags.
|
||||||
|
|||||||
+41
-8
@@ -714,6 +714,10 @@ table.permission-editor td.checkbox {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-title.deleted {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
.caption-panel {
|
.caption-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -956,6 +960,12 @@ i.search {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contact-box .contact-title.deleted {
|
||||||
|
opacity: 0.66;
|
||||||
|
text-decoration: line-through;
|
||||||
|
text-decoration-thickness: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
.contact-box .contact-comment {
|
.contact-box .contact-comment {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
@@ -1007,8 +1017,9 @@ i.search {
|
|||||||
height: 2rem;
|
height: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar.disabled {
|
.avatar.deleted {
|
||||||
opacity: 0.5;
|
opacity: 0.75;
|
||||||
|
filter: grayscale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-box .avatar-box > .material-icons {
|
.contact-box .avatar-box > .material-icons {
|
||||||
@@ -1093,22 +1104,24 @@ img.channel {
|
|||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
color: #0097A7;
|
color: #0097A7;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0.05rem;
|
right: 0;
|
||||||
bottom: 0.05rem;
|
bottom: 0;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
border: 0.0px solid transparent;
|
border: 0.0px solid transparent;
|
||||||
|
padding-left: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-box .deleted.material-icons {
|
.contact-box .deleted.material-icons {
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
color: gray;
|
color: #999;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0.05rem;
|
right: 0;
|
||||||
bottom: 0.05rem;
|
bottom: 0;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
padding-left: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* END of avatar box */
|
/* END of avatar box */
|
||||||
@@ -1437,7 +1450,8 @@ img.channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#send-message-panel .material-icons {
|
#send-message-panel .material-icons {
|
||||||
min-width: 1.75rem;
|
min-width: 2rem;
|
||||||
|
font-size: 1.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#send-message-panel .material-icons.secondary, .material-icons.disabled {
|
#send-message-panel .material-icons.secondary, .material-icons.disabled {
|
||||||
@@ -2626,6 +2640,25 @@ ul.tabbar li.active > a {
|
|||||||
height: 80%;
|
height: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.audio {
|
||||||
|
display: flex;
|
||||||
|
margin-left: auto;
|
||||||
|
align-items: center;
|
||||||
|
flex: none;
|
||||||
|
background-color: #eee;
|
||||||
|
height: 3rem;
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio canvas.visualiser {
|
||||||
|
width: 12rem;
|
||||||
|
height: 2.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio .duration {
|
||||||
|
font-size: large;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Mobile styles.
|
Mobile styles.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -33,9 +33,6 @@
|
|||||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@17/umd/react-dom.development.js"></script>
|
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@17/umd/react-dom.development.js"></script>
|
||||||
<!-- React-Intl for translations -->
|
<!-- React-Intl for translations -->
|
||||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5/react-intl.iife.js"></script>
|
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5/react-intl.iife.js"></script>
|
||||||
<!-- Firebase for push notifications -->
|
|
||||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@8/firebase-app.js"></script>
|
|
||||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@8/firebase-messaging.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -25,8 +25,6 @@
|
|||||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js"></script>
|
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.production.min.js"></script>
|
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.production.min.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5.20.7/react-intl.iife.min.js"></script>
|
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5.20.7/react-intl.iife.min.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@8.10.0/firebase-app.js"></script>
|
|
||||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@8.10.0/firebase-messaging.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
|
<noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
|
||||||
|
|||||||
+6
-1
@@ -36,5 +36,10 @@
|
|||||||
"platform": "itunes",
|
"platform": "itunes",
|
||||||
"url": "https://apps.apple.com/us/app/tinode/id1483763538"
|
"url": "https://apps.apple.com/us/app/tinode/id1483763538"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"permissions": {
|
||||||
|
"audio-capture": {
|
||||||
|
"description": "Required for recording voice messages"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1484
-1011
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "tinode-webapp",
|
"name": "tinode-webapp",
|
||||||
"description": "Tinode messenger for the web",
|
"description": "Tinode messenger for the web",
|
||||||
"version": "0.18.2-beta1",
|
"version": "0.19.0-alpha1",
|
||||||
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/tinode/example-react-js.git"
|
"url": "https://github.com/tinode/example-react-js.git"
|
||||||
@@ -66,11 +67,11 @@
|
|||||||
"author": "Tinode Authors <info@tinode.co>",
|
"author": "Tinode Authors <info@tinode.co>",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"firebase": "^8.10.0",
|
"firebase": "^9.6.7",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-intl": "^5.20.2",
|
"react-intl": "^5.20.2",
|
||||||
"tinode-sdk": "^0.18.1"
|
"tinode-sdk": "^0.19.0-alpha1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.14.5",
|
"@babel/core": "^7.14.5",
|
||||||
|
|||||||
+19
-7
@@ -1,6 +1,7 @@
|
|||||||
// Must be located at the root.
|
// Must be located at the root.
|
||||||
importScripts('https://cdn.jsdelivr.net/npm/firebase@8.9.1/firebase-app.js');
|
// FIXME: update Firebase version.
|
||||||
importScripts('https://cdn.jsdelivr.net/npm/firebase@8.9.1/firebase-messaging.js');
|
importScripts('https://cdn.jsdelivr.net/npm/firebase@8.10.1/firebase-app.js');
|
||||||
|
importScripts('https://cdn.jsdelivr.net/npm/firebase@8.10.1/firebase-messaging.js');
|
||||||
importScripts('firebase-init.js');
|
importScripts('firebase-init.js');
|
||||||
importScripts('version.js');
|
importScripts('version.js');
|
||||||
|
|
||||||
@@ -43,7 +44,11 @@ const i18n = {
|
|||||||
'new_chat': "新聊天",
|
'new_chat': "新聊天",
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.i18nMessage = function(id) {
|
self.i18nMessage = function(id) {
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
// Choose translations: given something like 'de-CH', try 'de-CH' then 'de' then 'en'.
|
// Choose translations: given something like 'de-CH', try 'de-CH' then 'de' then 'en'.
|
||||||
const lang = i18n[self.locale] || i18n[self.baseLocale] || i18n['en'];
|
const lang = i18n[self.locale] || i18n[self.baseLocale] || i18n['en'];
|
||||||
// Try finding string by id in the specified language, if missing try English, otherwise use the id itself
|
// Try finding string by id in the specified language, if missing try English, otherwise use the id itself
|
||||||
@@ -66,8 +71,10 @@ fbMessaging.onBackgroundMessage((payload) => {
|
|||||||
webAppChannel.postMessage(payload.data);
|
webAppChannel.postMessage(payload.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const titles = {'msg': 'new_message', 'sub': 'new_chat'};
|
||||||
const pushType = payload.data.what || 'msg';
|
const pushType = payload.data.what || 'msg';
|
||||||
const title = payload.data.title || self.i18nMessage(pushType == 'msg' ? 'new_message' : 'new_chat');
|
const title = payload.data.title || self.i18nMessage(titles[pushType]);
|
||||||
|
if (title) {
|
||||||
const options = {
|
const options = {
|
||||||
body: payload.data.content || '', // TODO: content for 'sub' should be topic's or user's title.
|
body: payload.data.content || '', // TODO: content for 'sub' should be topic's or user's title.
|
||||||
icon: '/img/logo96.png', // TODO: use topic's or user's avatar (would have to fetch for 'sub', read from db for 'msg').
|
icon: '/img/logo96.png', // TODO: use topic's or user's avatar (would have to fetch for 'sub', read from db for 'msg').
|
||||||
@@ -78,6 +85,7 @@ fbMessaging.onBackgroundMessage((payload) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return self.registration.showNotification(title, options);
|
return self.registration.showNotification(title, options);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update service worker immediately for both the current client
|
// Update service worker immediately for both the current client
|
||||||
@@ -89,10 +97,14 @@ self.addEventListener('install', event => {
|
|||||||
// This code handles a click on notification: takes
|
// This code handles a click on notification: takes
|
||||||
// the user to the browser tab with the chat or opens a new tab.
|
// the user to the browser tab with the chat or opens a new tab.
|
||||||
self.addEventListener('notificationclick', event => {
|
self.addEventListener('notificationclick', event => {
|
||||||
const notification = event.notification;
|
const data = event.notification.data;
|
||||||
notification.close();
|
event.notification.close();
|
||||||
|
if (!data) {
|
||||||
|
console.info("Missing 'data' in notification", event.notification);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const urlHash = '#/' + notification.data.topic;
|
const urlHash = '#/' + data.topic;
|
||||||
|
|
||||||
event.waitUntil(self.clients.matchAll({
|
event.waitUntil(self.clients.matchAll({
|
||||||
type: 'window',
|
type: 'window',
|
||||||
@@ -101,7 +113,7 @@ self.addEventListener('notificationclick', event => {
|
|||||||
let anyClient = null;
|
let anyClient = null;
|
||||||
for (let i = 0; i < windowClients.length; i++) {
|
for (let i = 0; i < windowClients.length; i++) {
|
||||||
const url = new URL(windowClients[i].url);
|
const url = new URL(windowClients[i].url);
|
||||||
if (url.hash.includes(notification.data.topic)) {
|
if (url.hash.includes(data.topic)) {
|
||||||
// Found the Tinode tab with the right topic open.
|
// Found the Tinode tab with the right topic open.
|
||||||
return windowClients[i].focus();
|
return windowClients[i].focus();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+10
-3
@@ -324,21 +324,21 @@
|
|||||||
"drafty_attachment": {
|
"drafty_attachment": {
|
||||||
"translation": "Dateianhang",
|
"translation": "Dateianhang",
|
||||||
"defaultMessage": "Attachment",
|
"defaultMessage": "Attachment",
|
||||||
"description": "Comment for attachment in drafty preview",
|
"description": "Comment for attachment in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_form": {
|
"drafty_form": {
|
||||||
"translation": "Formular:",
|
"translation": "Formular:",
|
||||||
"defaultMessage": "Form:",
|
"defaultMessage": "Form:",
|
||||||
"description": "Comment for form in drafty preview",
|
"description": "Comment for form in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_image": {
|
"drafty_image": {
|
||||||
"translation": "Bild",
|
"translation": "Bild",
|
||||||
"defaultMessage": "Picture",
|
"defaultMessage": "Picture",
|
||||||
"description": "Comment for embedded images in drafty preview",
|
"description": "Comment for embedded images in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
@@ -1360,5 +1360,12 @@
|
|||||||
"description": "Label for entering a new password",
|
"description": "Label for entering a new password",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"drafty_unknown": {
|
||||||
|
"translation": "",
|
||||||
|
"defaultMessage": "Unsupported",
|
||||||
|
"description": "Unsupported entity in drafty",
|
||||||
|
"missing": true,
|
||||||
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -324,21 +324,21 @@
|
|||||||
"drafty_attachment": {
|
"drafty_attachment": {
|
||||||
"translation": "Attachment",
|
"translation": "Attachment",
|
||||||
"defaultMessage": "Attachment",
|
"defaultMessage": "Attachment",
|
||||||
"description": "Comment for attachment in drafty preview",
|
"description": "Comment for attachment in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_form": {
|
"drafty_form": {
|
||||||
"translation": "Form:",
|
"translation": "Form:",
|
||||||
"defaultMessage": "Form:",
|
"defaultMessage": "Form:",
|
||||||
"description": "Comment for form in drafty preview",
|
"description": "Comment for form in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_image": {
|
"drafty_image": {
|
||||||
"translation": "Picture",
|
"translation": "Picture",
|
||||||
"defaultMessage": "Picture",
|
"defaultMessage": "Picture",
|
||||||
"description": "Comment for embedded images in drafty preview",
|
"description": "Comment for embedded images in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
@@ -1360,5 +1360,12 @@
|
|||||||
"description": "Label for entering a new password",
|
"description": "Label for entering a new password",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"drafty_unknown": {
|
||||||
|
"translation": "Unsupported",
|
||||||
|
"defaultMessage": "Unsupported",
|
||||||
|
"description": "Unsupported entity in drafty",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -324,21 +324,21 @@
|
|||||||
"drafty_attachment": {
|
"drafty_attachment": {
|
||||||
"translation": "Archivo",
|
"translation": "Archivo",
|
||||||
"defaultMessage": "Attachment",
|
"defaultMessage": "Attachment",
|
||||||
"description": "Comment for attachment in drafty preview",
|
"description": "Comment for attachment in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_form": {
|
"drafty_form": {
|
||||||
"translation": "Formulario:",
|
"translation": "Formulario:",
|
||||||
"defaultMessage": "Form:",
|
"defaultMessage": "Form:",
|
||||||
"description": "Comment for form in drafty preview",
|
"description": "Comment for form in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_image": {
|
"drafty_image": {
|
||||||
"translation": "Imagen",
|
"translation": "Imagen",
|
||||||
"defaultMessage": "Picture",
|
"defaultMessage": "Picture",
|
||||||
"description": "Comment for embedded images in drafty preview",
|
"description": "Comment for embedded images in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
@@ -1360,5 +1360,12 @@
|
|||||||
"description": "Label for entering a new password",
|
"description": "Label for entering a new password",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"drafty_unknown": {
|
||||||
|
"translation": "",
|
||||||
|
"defaultMessage": "Unsupported",
|
||||||
|
"description": "Unsupported entity in drafty",
|
||||||
|
"missing": true,
|
||||||
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,15 +193,19 @@
|
|||||||
},
|
},
|
||||||
"drafty_attachment": {
|
"drafty_attachment": {
|
||||||
"defaultMessage": "Attachment",
|
"defaultMessage": "Attachment",
|
||||||
"description": "Comment for attachment in drafty preview"
|
"description": "Comment for attachment in drafty"
|
||||||
},
|
},
|
||||||
"drafty_form": {
|
"drafty_form": {
|
||||||
"defaultMessage": "Form:",
|
"defaultMessage": "Form:",
|
||||||
"description": "Comment for form in drafty preview"
|
"description": "Comment for form in drafty"
|
||||||
},
|
},
|
||||||
"drafty_image": {
|
"drafty_image": {
|
||||||
"defaultMessage": "Picture",
|
"defaultMessage": "Picture",
|
||||||
"description": "Comment for embedded images in drafty preview"
|
"description": "Comment for embedded images in drafty"
|
||||||
|
},
|
||||||
|
"drafty_unknown": {
|
||||||
|
"defaultMessage": "Unsupported",
|
||||||
|
"description": "Unsupported entity in drafty"
|
||||||
},
|
},
|
||||||
"email_dative": {
|
"email_dative": {
|
||||||
"defaultMessage": "email",
|
"defaultMessage": "email",
|
||||||
|
|||||||
+10
-3
@@ -324,21 +324,21 @@
|
|||||||
"drafty_attachment": {
|
"drafty_attachment": {
|
||||||
"translation": "부착",
|
"translation": "부착",
|
||||||
"defaultMessage": "Attachment",
|
"defaultMessage": "Attachment",
|
||||||
"description": "Comment for attachment in drafty preview",
|
"description": "Comment for attachment in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_form": {
|
"drafty_form": {
|
||||||
"translation": "형태:",
|
"translation": "형태:",
|
||||||
"defaultMessage": "Form:",
|
"defaultMessage": "Form:",
|
||||||
"description": "Comment for form in drafty preview",
|
"description": "Comment for form in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_image": {
|
"drafty_image": {
|
||||||
"translation": "이미지",
|
"translation": "이미지",
|
||||||
"defaultMessage": "Picture",
|
"defaultMessage": "Picture",
|
||||||
"description": "Comment for embedded images in drafty preview",
|
"description": "Comment for embedded images in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
@@ -1360,5 +1360,12 @@
|
|||||||
"description": "Label for entering a new password",
|
"description": "Label for entering a new password",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"drafty_unknown": {
|
||||||
|
"translation": "",
|
||||||
|
"defaultMessage": "Unsupported",
|
||||||
|
"description": "Unsupported entity in drafty",
|
||||||
|
"missing": true,
|
||||||
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -324,21 +324,21 @@
|
|||||||
"drafty_attachment": {
|
"drafty_attachment": {
|
||||||
"translation": "Atasament",
|
"translation": "Atasament",
|
||||||
"defaultMessage": "Attachment",
|
"defaultMessage": "Attachment",
|
||||||
"description": "Comment for attachment in drafty preview",
|
"description": "Comment for attachment in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_form": {
|
"drafty_form": {
|
||||||
"translation": "Formă:",
|
"translation": "Formă:",
|
||||||
"defaultMessage": "Form:",
|
"defaultMessage": "Form:",
|
||||||
"description": "Comment for form in drafty preview",
|
"description": "Comment for form in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_image": {
|
"drafty_image": {
|
||||||
"translation": "Imagine",
|
"translation": "Imagine",
|
||||||
"defaultMessage": "Picture",
|
"defaultMessage": "Picture",
|
||||||
"description": "Comment for embedded images in drafty preview",
|
"description": "Comment for embedded images in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
@@ -1360,5 +1360,12 @@
|
|||||||
"description": "Label for entering a new password",
|
"description": "Label for entering a new password",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"drafty_unknown": {
|
||||||
|
"translation": "",
|
||||||
|
"defaultMessage": "Unsupported",
|
||||||
|
"description": "Unsupported entity in drafty",
|
||||||
|
"missing": true,
|
||||||
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -324,21 +324,21 @@
|
|||||||
"drafty_attachment": {
|
"drafty_attachment": {
|
||||||
"translation": "Аттачмент",
|
"translation": "Аттачмент",
|
||||||
"defaultMessage": "Attachment",
|
"defaultMessage": "Attachment",
|
||||||
"description": "Comment for attachment in drafty preview",
|
"description": "Comment for attachment in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_form": {
|
"drafty_form": {
|
||||||
"translation": "Форма:",
|
"translation": "Форма:",
|
||||||
"defaultMessage": "Form:",
|
"defaultMessage": "Form:",
|
||||||
"description": "Comment for form in drafty preview",
|
"description": "Comment for form in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_image": {
|
"drafty_image": {
|
||||||
"translation": "Картинка",
|
"translation": "Картинка",
|
||||||
"defaultMessage": "Picture",
|
"defaultMessage": "Picture",
|
||||||
"description": "Comment for embedded images in drafty preview",
|
"description": "Comment for embedded images in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
@@ -1360,5 +1360,12 @@
|
|||||||
"description": "Label for entering a new password",
|
"description": "Label for entering a new password",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"drafty_unknown": {
|
||||||
|
"translation": "",
|
||||||
|
"defaultMessage": "Unsupported",
|
||||||
|
"description": "Unsupported entity in drafty",
|
||||||
|
"missing": true,
|
||||||
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -324,21 +324,21 @@
|
|||||||
"drafty_attachment": {
|
"drafty_attachment": {
|
||||||
"translation": "附加檔案",
|
"translation": "附加檔案",
|
||||||
"defaultMessage": "Attachment",
|
"defaultMessage": "Attachment",
|
||||||
"description": "Comment for attachment in drafty preview",
|
"description": "Comment for attachment in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_form": {
|
"drafty_form": {
|
||||||
"translation": "表單:",
|
"translation": "表單:",
|
||||||
"defaultMessage": "Form:",
|
"defaultMessage": "Form:",
|
||||||
"description": "Comment for form in drafty preview",
|
"description": "Comment for form in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_image": {
|
"drafty_image": {
|
||||||
"translation": "圖片",
|
"translation": "圖片",
|
||||||
"defaultMessage": "Picture",
|
"defaultMessage": "Picture",
|
||||||
"description": "Comment for embedded images in drafty preview",
|
"description": "Comment for embedded images in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
@@ -1360,5 +1360,12 @@
|
|||||||
"description": "Label for entering a new password",
|
"description": "Label for entering a new password",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"drafty_unknown": {
|
||||||
|
"translation": "",
|
||||||
|
"defaultMessage": "Unsupported",
|
||||||
|
"description": "Unsupported entity in drafty",
|
||||||
|
"missing": true,
|
||||||
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -324,21 +324,21 @@
|
|||||||
"drafty_attachment": {
|
"drafty_attachment": {
|
||||||
"translation": "附件",
|
"translation": "附件",
|
||||||
"defaultMessage": "Attachment",
|
"defaultMessage": "Attachment",
|
||||||
"description": "Comment for attachment in drafty preview",
|
"description": "Comment for attachment in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_form": {
|
"drafty_form": {
|
||||||
"translation": "形式:",
|
"translation": "形式:",
|
||||||
"defaultMessage": "Form:",
|
"defaultMessage": "Form:",
|
||||||
"description": "Comment for form in drafty preview",
|
"description": "Comment for form in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_image": {
|
"drafty_image": {
|
||||||
"translation": "图像",
|
"translation": "图像",
|
||||||
"defaultMessage": "Picture",
|
"defaultMessage": "Picture",
|
||||||
"description": "Comment for embedded images in drafty preview",
|
"description": "Comment for embedded images in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
@@ -1360,5 +1360,12 @@
|
|||||||
"description": "Label for entering a new password",
|
"description": "Label for entering a new password",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"drafty_unknown": {
|
||||||
|
"translation": "",
|
||||||
|
"defaultMessage": "Unsupported",
|
||||||
|
"description": "Unsupported entity in drafty",
|
||||||
|
"missing": true,
|
||||||
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ export function fileToBase64(file) {
|
|||||||
export function blobToBase64(blob) {
|
export function blobToBase64(blob) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onerror = (evt) => {
|
reader.onerror = _ => {
|
||||||
reject(reader.error);
|
reject(reader.error);
|
||||||
};
|
};
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
|
|||||||
+20
-53
@@ -15,20 +15,33 @@ const messages = defineMessages({
|
|||||||
drafty_form: {
|
drafty_form: {
|
||||||
id: 'drafty_form',
|
id: 'drafty_form',
|
||||||
defaultMessage: 'Form: ',
|
defaultMessage: 'Form: ',
|
||||||
description: 'Comment for form in drafty preview'
|
description: 'Comment for form in drafty'
|
||||||
},
|
},
|
||||||
drafty_attachment: {
|
drafty_attachment: {
|
||||||
id: 'drafty_attachment',
|
id: 'drafty_attachment',
|
||||||
defaultMessage: 'Attachment',
|
defaultMessage: 'Attachment',
|
||||||
description: 'Comment for attachment in drafty preview'
|
description: 'Comment for attachment in drafty'
|
||||||
},
|
},
|
||||||
drafty_image: {
|
drafty_image: {
|
||||||
id: 'drafty_image',
|
id: 'drafty_image',
|
||||||
defaultMessage: 'Picture',
|
defaultMessage: 'Picture',
|
||||||
description: 'Comment for embedded images in drafty preview'
|
description: 'Comment for embedded images in drafty'
|
||||||
|
},
|
||||||
|
drafty_unknown: {
|
||||||
|
id: 'drafty_unknown',
|
||||||
|
defaultMessage: 'Unsupported',
|
||||||
|
description: 'Unsupported entity in drafty'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get comment for unknown element
|
||||||
|
function unsupportedComment(values, ifMissing) {
|
||||||
|
if (Array.isArray(values) && values.join('').trim()) {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
return [<span className="gray">{ifMissing}</span>];
|
||||||
|
}
|
||||||
|
|
||||||
// Size the already scaled image.
|
// Size the already scaled image.
|
||||||
function handleImageData(el, data, attr) {
|
function handleImageData(el, data, attr) {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -73,50 +86,6 @@ function handleImageData(el, data, attr) {
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
function quotedImage(data) {
|
|
||||||
let promise;
|
|
||||||
// Get the blob from the image data.
|
|
||||||
if (data.val) {
|
|
||||||
const blob = base64ToBlob(data.val, data.mime);
|
|
||||||
promise = blob ? Promise.resolve(blob) : Prmise.reject(new Error("Invalid image"));
|
|
||||||
} else {
|
|
||||||
promise = fetch(this.authorizeURL(sanitizeImageUrl(data.ref))).then(evt => {
|
|
||||||
if (evt.ok) {
|
|
||||||
return evt.blob();
|
|
||||||
} else {
|
|
||||||
throw new Error(`Image fetch unsuccessful: ${evt.status} ${evt.statusText}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scale the blob.
|
|
||||||
return promise
|
|
||||||
.then(blob => {
|
|
||||||
return imageScaled(blob, IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, true)
|
|
||||||
}).then(scaled => {
|
|
||||||
data.mime = scaled.mime;
|
|
||||||
data.size = scaled.blob.size;
|
|
||||||
data.width = scaled.width;
|
|
||||||
data.height = scaled.height;
|
|
||||||
delete data.ref;
|
|
||||||
// Keeping the original file name, if provided: ex.data.name;
|
|
||||||
|
|
||||||
return blobToBase64(scaled.blob);
|
|
||||||
}).then(b64 => {
|
|
||||||
data.val = b64.bits;
|
|
||||||
return data;
|
|
||||||
}).catch(err => {
|
|
||||||
delete data.val;
|
|
||||||
delete data.name;
|
|
||||||
data.width = IMAGE_THUMBNAIL_DIM;
|
|
||||||
data.height = IMAGE_THUMBNAIL_DIM;
|
|
||||||
data.maxWidth = IMAGE_THUMBNAIL_DIM;
|
|
||||||
data.maxHeight = IMAGE_THUMBNAIL_DIM;
|
|
||||||
// Rethrow.
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// The main Drafty formatter: converts Drafty elements into React classes. 'this' is set by the caller.
|
// The main Drafty formatter: converts Drafty elements into React classes. 'this' is set by the caller.
|
||||||
// 'this' must contain:
|
// 'this' must contain:
|
||||||
// viewportWidth: this.props.viewportWidth;
|
// viewportWidth: this.props.viewportWidth;
|
||||||
@@ -187,11 +156,10 @@ export function fullFormatter(style, data, values, key, stack) {
|
|||||||
attr.onClick = this.onQuoteClick;
|
attr.onClick = this.onQuoteClick;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (el == '_UNKN') {
|
|
||||||
// Unknown element.
|
// Unknown element.
|
||||||
el = React.Fragment;
|
el = React.Fragment;
|
||||||
values = [<i className="material-icons gray">extension</i>, ' '].concat(values || []);
|
values = [<i className="material-icons gray">extension</i>, ' ']
|
||||||
}
|
.concat(unsupportedComment(values, this.formatMessage(messages.drafty_unknown)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!el) {
|
if (!el) {
|
||||||
@@ -263,10 +231,9 @@ export function previewFormatter(style, data, values, key) {
|
|||||||
values = null;
|
values = null;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (el == '_UNKN') {
|
|
||||||
el = React.Fragment;
|
el = React.Fragment;
|
||||||
values = [<i key="unkn" className="material-icons">extension</i>, ' '].concat(values || []);
|
values = [<i className="material-icons gray">extension</i>, ' ']
|
||||||
}
|
.concat(unsupportedComment(values, this.formatMessage(messages.drafty_unknown)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!el) {
|
if (!el) {
|
||||||
|
|||||||
@@ -17,9 +17,13 @@ export function shortDateFormat(then, locale) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert seconds to minutes:seconds, i.e. 156 sec -> 2:36.
|
// Convert seconds to minutes:seconds, i.e. 156 sec -> 2:36.
|
||||||
export function secondsToTime(seconds) {
|
// If <code>fixedMins</code> is true, then minutes are represented by at least two digits.
|
||||||
const min = Math.floor(seconds / 60);
|
export function secondsToTime(seconds, fixedMin) {
|
||||||
let sec = seconds % 60;
|
let min = Math.floor(seconds / 60);
|
||||||
|
if (fixedMin) {
|
||||||
|
min = min < 10 ? `0${min}` : min;
|
||||||
|
}
|
||||||
|
let sec = (seconds % 60) | 0;
|
||||||
sec = sec < 10 ? `0${sec}` : sec;
|
sec = sec < 10 ? `0${sec}` : sec;
|
||||||
return `${min}:${sec}`;
|
return `${min}:${sec}`;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
// Odds and ends
|
// Odds and ends
|
||||||
|
|
||||||
import Tinode from 'tinode-sdk';
|
import { Tinode } from 'tinode-sdk';
|
||||||
|
|
||||||
// Make shortcut icon appear with a green dot + show unread count in title.
|
// Make shortcut icon appear with a green dot + show unread count in title.
|
||||||
export function updateFavicon(count) {
|
export function updateFavicon(count) {
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
// This is a generated file. Don't edit.
|
// This is a generated file. Don't edit.
|
||||||
|
|
||||||
export const PACKAGE_VERSION = "0.18.2-beta1";
|
export const PACKAGE_VERSION = "0.19.0-alpha1";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import Tinode from 'tinode-sdk';
|
import { Tinode } from 'tinode-sdk';
|
||||||
|
|
||||||
import { APP_NAME, LINK_CONTACT_US, LINK_PRIVACY_POLICY, LINK_TERMS_OF_SERVICE } from '../config.js';
|
import { APP_NAME, LINK_CONTACT_US, LINK_PRIVACY_POLICY, LINK_TERMS_OF_SERVICE } from '../config.js';
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import Tinode from 'tinode-sdk';
|
import { Tinode } from 'tinode-sdk';
|
||||||
|
|
||||||
import AvatarUpload from '../widgets/avatar-upload.jsx';
|
import AvatarUpload from '../widgets/avatar-upload.jsx';
|
||||||
import BadgeList from '../widgets/badge-list.jsx';
|
import BadgeList from '../widgets/badge-list.jsx';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import Tinode from 'tinode-sdk';
|
import { Tinode } from 'tinode-sdk';
|
||||||
|
|
||||||
import { APP_NAME } from '../config.js';
|
import { APP_NAME } from '../config.js';
|
||||||
|
|
||||||
|
|||||||
+64
-11
@@ -1,8 +1,9 @@
|
|||||||
|
// Panel with a chat.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import Tinode from 'tinode-sdk';
|
import { Drafty, Tinode } from 'tinode-sdk';
|
||||||
const Drafty = Tinode.Drafty;
|
|
||||||
|
|
||||||
import ChatMessage from '../widgets/chat-message.jsx';
|
import ChatMessage from '../widgets/chat-message.jsx';
|
||||||
import ContactBadges from '../widgets/contact-badges.jsx';
|
import ContactBadges from '../widgets/contact-badges.jsx';
|
||||||
@@ -90,6 +91,7 @@ class MessagesView extends React.Component {
|
|||||||
this.retrySend = this.retrySend.bind(this);
|
this.retrySend = this.retrySend.bind(this);
|
||||||
this.sendImageAttachment = this.sendImageAttachment.bind(this);
|
this.sendImageAttachment = this.sendImageAttachment.bind(this);
|
||||||
this.sendFileAttachment = this.sendFileAttachment.bind(this);
|
this.sendFileAttachment = this.sendFileAttachment.bind(this);
|
||||||
|
this.sendAudioAttachment = this.sendAudioAttachment.bind(this);
|
||||||
this.sendKeyPress = this.sendKeyPress.bind(this);
|
this.sendKeyPress = this.sendKeyPress.bind(this);
|
||||||
this.subscribe = this.subscribe.bind(this);
|
this.subscribe = this.subscribe.bind(this);
|
||||||
this.handleScrollReference = this.handleScrollReference.bind(this);
|
this.handleScrollReference = this.handleScrollReference.bind(this);
|
||||||
@@ -185,10 +187,15 @@ class MessagesView extends React.Component {
|
|||||||
this.postReadNotification(0);
|
this.postReadNotification(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((this.state.topic != prevState.topic) || !prevProps.ready) {
|
if (topic && ((this.state.topic != prevState.topic) || !prevProps.ready)) {
|
||||||
|
if (topic._new) {
|
||||||
|
console.log('DEBUG: Fetching new topic description');
|
||||||
|
// topic.getMeta(topic.startMetaQuery().withDesc().build());
|
||||||
|
} else {
|
||||||
this.subscribe(topic);
|
this.subscribe(topic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(nextProps, prevState) {
|
static getDerivedStateFromProps(nextProps, prevState) {
|
||||||
let nextState = {};
|
let nextState = {};
|
||||||
@@ -204,6 +211,7 @@ class MessagesView extends React.Component {
|
|||||||
isVerified: false,
|
isVerified: false,
|
||||||
isStaff: false,
|
isStaff: false,
|
||||||
isDangerous: false,
|
isDangerous: false,
|
||||||
|
deleted: false,
|
||||||
docPreview: null,
|
docPreview: null,
|
||||||
imagePreview: null,
|
imagePreview: null,
|
||||||
imagePostview: null,
|
imagePostview: null,
|
||||||
@@ -237,7 +245,8 @@ class MessagesView extends React.Component {
|
|||||||
scrollPosition: 0,
|
scrollPosition: 0,
|
||||||
fetchingMessages: false,
|
fetchingMessages: false,
|
||||||
reply: reply,
|
reply: reply,
|
||||||
showGoToLastButton: false
|
showGoToLastButton: false,
|
||||||
|
deleted: topic._deleted
|
||||||
};
|
};
|
||||||
|
|
||||||
if (topic) {
|
if (topic) {
|
||||||
@@ -338,7 +347,7 @@ class MessagesView extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
subscribe(topic) {
|
subscribe(topic) {
|
||||||
if (!topic || topic.isSubscribed() || !this.props.ready) {
|
if (topic.isSubscribed() || !this.props.ready) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,6 +744,10 @@ class MessagesView extends React.Component {
|
|||||||
if (file.size > maxInbandAttachmentSize) {
|
if (file.size > maxInbandAttachmentSize) {
|
||||||
// Too large to send inband - uploading out of band and sending as a link.
|
// Too large to send inband - uploading out of band and sending as a link.
|
||||||
const uploader = this.props.tinode.getLargeFileHelper();
|
const uploader = this.props.tinode.getLargeFileHelper();
|
||||||
|
if (!uploader) {
|
||||||
|
this.props.onError(this.props.intl.formatMessage(messages.cannot_initiate_upload));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const uploadCompletionPromise = uploader.upload(file);
|
const uploadCompletionPromise = uploader.upload(file);
|
||||||
const msg = Drafty.attachFile(null, {
|
const msg = Drafty.attachFile(null, {
|
||||||
mime: file.type,
|
mime: file.type,
|
||||||
@@ -752,7 +765,7 @@ class MessagesView extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleAttachFile method is called when [Attach file] button is clicked.
|
// handleAttachFile method is called when [Attach file] button is clicked: launch attachment preview.
|
||||||
handleAttachFile(file) {
|
handleAttachFile(file) {
|
||||||
const maxExternAttachmentSize = this.props.tinode.getServerLimit('maxFileUploadSize', MAX_EXTERN_ATTACHMENT_SIZE);
|
const maxExternAttachmentSize = this.props.tinode.getServerLimit('maxFileUploadSize', MAX_EXTERN_ATTACHMENT_SIZE);
|
||||||
|
|
||||||
@@ -837,7 +850,7 @@ class MessagesView extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleAttachImage method is called when [Attach image] button is clicked.
|
// handleAttachImage method is called when [Attach image] button is clicked: launch image preview.
|
||||||
handleAttachImage(file) {
|
handleAttachImage(file) {
|
||||||
const maxExternAttachmentSize = this.props.tinode.getServerLimit('maxFileUploadSize', MAX_EXTERN_ATTACHMENT_SIZE);
|
const maxExternAttachmentSize = this.props.tinode.getServerLimit('maxFileUploadSize', MAX_EXTERN_ATTACHMENT_SIZE);
|
||||||
|
|
||||||
@@ -858,6 +871,41 @@ class MessagesView extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendAudioAttachment sends audio bits inband as Drafty message (no preview).
|
||||||
|
sendAudioAttachment(url, duration) {
|
||||||
|
// Server-provided limit reduced for base64 encoding and overhead.
|
||||||
|
const maxInbandAttachmentSize = (this.props.tinode.getServerLimit(
|
||||||
|
'maxMessageSize', MAX_INBAND_ATTACHMENT_SIZE) * 0.75 - 1024) | 0;
|
||||||
|
fetch(url).then(result => {
|
||||||
|
return result.blob();
|
||||||
|
}).then(blob => {
|
||||||
|
if (blob.size > maxInbandAttachmentSize) {
|
||||||
|
// Too large to send inband - uploading out of band and sending as a link.
|
||||||
|
const uploader = this.props.tinode.getLargeFileHelper();
|
||||||
|
if (!uploader) {
|
||||||
|
this.props.onError(this.props.intl.formatMessage(messages.cannot_initiate_upload));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uploadCompletionPromise = uploader.upload(blob);
|
||||||
|
const msg = Drafty.appendAudio(null, {
|
||||||
|
mime: blob.type,
|
||||||
|
size: blob.size,
|
||||||
|
duration: duration,
|
||||||
|
urlPromise: uploadCompletionPromise
|
||||||
|
});
|
||||||
|
// Pass data and the uploader to the TinodeWeb.
|
||||||
|
this.sendMessage(msg, uploadCompletionPromise, uploader);
|
||||||
|
} else {
|
||||||
|
// Small enough to send inband.
|
||||||
|
blobToBase64(blob)
|
||||||
|
.then(b64 => {
|
||||||
|
this.sendMessage(Drafty.appendAudio(null, {mime: b64.mime, data: b64.bits, duration: duration}))
|
||||||
|
})
|
||||||
|
.catch(err => {this.props.onError(err)});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
handleCancelUpload(seq, uploader) {
|
handleCancelUpload(seq, uploader) {
|
||||||
const topic = this.props.tinode.getTopic(this.state.topic);
|
const topic = this.props.tinode.getTopic(this.state.topic);
|
||||||
const found = topic.findMessage(seq);
|
const found = topic.findMessage(seq);
|
||||||
@@ -1077,7 +1125,10 @@ class MessagesView extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const avatar = this.state.avatar || true;
|
const avatar = this.state.avatar || true;
|
||||||
const online = this.props.online ? 'online' + (this.state.typingIndicator ? ' typing' : '') : 'offline';
|
const online = this.state.deleted ? null :
|
||||||
|
this.props.online ? 'online' + (this.state.typingIndicator ? ' typing' : '') : 'offline';
|
||||||
|
|
||||||
|
const titleClass = 'panel-title' + (this.state.deleted ? ' deleted' : '');
|
||||||
|
|
||||||
component2 = (
|
component2 = (
|
||||||
<>
|
<>
|
||||||
@@ -1093,11 +1144,12 @@ class MessagesView extends React.Component {
|
|||||||
tinode={this.props.tinode}
|
tinode={this.props.tinode}
|
||||||
avatar={avatar}
|
avatar={avatar}
|
||||||
topic={this.state.topic}
|
topic={this.state.topic}
|
||||||
title={this.state.title} />
|
title={this.state.title}
|
||||||
|
deleted={this.state.deleted} />
|
||||||
{!isChannel ? <span className={online} /> : null}
|
{!isChannel ? <span className={online} /> : null}
|
||||||
</div>
|
</div>
|
||||||
<div id="topic-title-group">
|
<div id="topic-title-group">
|
||||||
<div id="topic-title" className="panel-title">{
|
<div id="topic-title" className={titleClass}>{
|
||||||
this.state.title ||
|
this.state.title ||
|
||||||
<i><FormattedMessage id="unnamed_topic" defaultMessage="Unnamed"
|
<i><FormattedMessage id="unnamed_topic" defaultMessage="Unnamed"
|
||||||
description="Title shown when the topic has no name" /></i>
|
description="Title shown when the topic has no name" /></i>
|
||||||
@@ -1158,11 +1210,12 @@ class MessagesView extends React.Component {
|
|||||||
<SendMessage
|
<SendMessage
|
||||||
tinode={this.props.tinode}
|
tinode={this.props.tinode}
|
||||||
noInput={!!this.props.forwardMessage}
|
noInput={!!this.props.forwardMessage}
|
||||||
disabled={!this.state.isWriter || topic.deleted}
|
disabled={!this.state.isWriter || this.state.deleted}
|
||||||
onKeyPress={this.sendKeyPress}
|
onKeyPress={this.sendKeyPress}
|
||||||
onSendMessage={this.sendMessage}
|
onSendMessage={this.sendMessage}
|
||||||
onAttachFile={this.props.forwardMessage ? null : this.handleAttachFile}
|
onAttachFile={this.props.forwardMessage ? null : this.handleAttachFile}
|
||||||
onAttachImage={this.props.forwardMessage ? null : this.handleAttachImage}
|
onAttachImage={this.props.forwardMessage ? null : this.handleAttachImage}
|
||||||
|
onAttachAudio={this.sendAudioAttachment}
|
||||||
onError={this.props.onError}
|
onError={this.props.onError}
|
||||||
reply={this.state.reply}
|
reply={this.state.reply}
|
||||||
onQuoteClick={this.handleQuoteClick}
|
onQuoteClick={this.handleQuoteClick}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import Tinode from 'tinode-sdk';
|
import { Tinode } from 'tinode-sdk';
|
||||||
|
|
||||||
import ContactList from '../widgets/contact-list.jsx';
|
import ContactList from '../widgets/contact-list.jsx';
|
||||||
import NewTopicById from '../widgets/new-topic-by-id.jsx';
|
import NewTopicById from '../widgets/new-topic-by-id.jsx';
|
||||||
@@ -41,7 +41,7 @@ class NewTopicView extends React.Component {
|
|||||||
|
|
||||||
this.handleTabClick = this.handleTabClick.bind(this);
|
this.handleTabClick = this.handleTabClick.bind(this);
|
||||||
this.handleSearchContacts = this.handleSearchContacts.bind(this);
|
this.handleSearchContacts = this.handleSearchContacts.bind(this);
|
||||||
this.handleContactSelected = this.handleContactSelected.bind(this);
|
this.handleSearchResultSelected = this.handleSearchResultSelected.bind(this);
|
||||||
this.handleNewGroupSubmit = this.handleNewGroupSubmit.bind(this);
|
this.handleNewGroupSubmit = this.handleNewGroupSubmit.bind(this);
|
||||||
this.handleGroupByID = this.handleGroupByID.bind(this);
|
this.handleGroupByID = this.handleGroupByID.bind(this);
|
||||||
}
|
}
|
||||||
@@ -61,10 +61,10 @@ class NewTopicView extends React.Component {
|
|||||||
this.setState({searchQuery: Tinode.isNullValue(query) ? null : query});
|
this.setState({searchQuery: Tinode.isNullValue(query) ? null : query});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleContactSelected(uid) {
|
handleSearchResultSelected(name) {
|
||||||
if (this.state.tabSelected == 'find') {
|
if (this.state.tabSelected == 'find') {
|
||||||
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
|
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
|
||||||
this.props.onCreateTopic(uid);
|
this.props.onCreateTopic(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ class NewTopicView extends React.Component {
|
|||||||
showOnline={false}
|
showOnline={false}
|
||||||
showUnread={false}
|
showUnread={false}
|
||||||
showContextMenu={false}
|
showContextMenu={false}
|
||||||
onTopicSelected={this.handleContactSelected} />
|
onTopicSelected={this.handleSearchResultSelected} />
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+103
-91
@@ -2,10 +2,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import * as firebase from 'firebase/app';
|
import { initializeApp as firebaseInitApp } from 'firebase/app';
|
||||||
import 'firebase/messaging';
|
import { getMessaging as firebaseGetMessaging, getToken as firebaseGetToken,
|
||||||
|
deleteToken as firebaseDelToken, onMessage as firebaseOnMessage } from 'firebase/messaging';
|
||||||
|
|
||||||
import Tinode from 'tinode-sdk';
|
import { Drafty, Tinode } from 'tinode-sdk';
|
||||||
|
|
||||||
import Alert from '../widgets/alert.jsx';
|
import Alert from '../widgets/alert.jsx';
|
||||||
import ContextMenu from '../widgets/context-menu.jsx';
|
import ContextMenu from '../widgets/context-menu.jsx';
|
||||||
@@ -114,8 +115,7 @@ class TinodeWeb extends React.Component {
|
|||||||
this.handleCredDelete = this.handleCredDelete.bind(this);
|
this.handleCredDelete = this.handleCredDelete.bind(this);
|
||||||
this.handleCredConfirm = this.handleCredConfirm.bind(this);
|
this.handleCredConfirm = this.handleCredConfirm.bind(this);
|
||||||
this.initFCMessaging = this.initFCMessaging.bind(this);
|
this.initFCMessaging = this.initFCMessaging.bind(this);
|
||||||
this.togglePushToken = this.togglePushToken.bind(this);
|
this.toggleFCMToken = this.toggleFCMToken.bind(this);
|
||||||
this.requestPushToken = this.requestPushToken.bind(this);
|
|
||||||
this.handlePushMessage = this.handlePushMessage.bind(this);
|
this.handlePushMessage = this.handlePushMessage.bind(this);
|
||||||
this.handleSidepanelCancel = this.handleSidepanelCancel.bind(this);
|
this.handleSidepanelCancel = this.handleSidepanelCancel.bind(this);
|
||||||
this.handleStartTopicRequest = this.handleStartTopicRequest.bind(this);
|
this.handleStartTopicRequest = this.handleStartTopicRequest.bind(this);
|
||||||
@@ -164,9 +164,11 @@ class TinodeWeb extends React.Component {
|
|||||||
// "On" is the default, so saving the "off" state.
|
// "On" is the default, so saving the "off" state.
|
||||||
messageSounds: !settings.messageSoundsOff,
|
messageSounds: !settings.messageSoundsOff,
|
||||||
incognitoMode: false,
|
incognitoMode: false,
|
||||||
|
// Persistent request to enable alerts.
|
||||||
desktopAlerts: persist && !!settings.desktopAlerts,
|
desktopAlerts: persist && !!settings.desktopAlerts,
|
||||||
|
// Enable / disable checkbox.
|
||||||
desktopAlertsEnabled: (isSecureConnection() || isLocalHost()) &&
|
desktopAlertsEnabled: (isSecureConnection() || isLocalHost()) &&
|
||||||
(typeof firebase != 'undefined') && (typeof navigator != 'undefined') &&
|
(typeof firebaseInitApp != 'undefined') && (typeof navigator != 'undefined') &&
|
||||||
(typeof FIREBASE_INIT != 'undefined'),
|
(typeof FIREBASE_INIT != 'undefined'),
|
||||||
firebaseToken: persist ? LocalStorageUtil.getObject('firebase-token') : null,
|
firebaseToken: persist ? LocalStorageUtil.getObject('firebase-token') : null,
|
||||||
|
|
||||||
@@ -257,12 +259,8 @@ class TinodeWeb extends React.Component {
|
|||||||
if (this.state.desktopAlertsEnabled) {
|
if (this.state.desktopAlertsEnabled) {
|
||||||
this.initFCMessaging().then(() => {
|
this.initFCMessaging().then(() => {
|
||||||
if (this.state.desktopAlerts) {
|
if (this.state.desktopAlerts) {
|
||||||
if (!this.state.firebaseToken) {
|
|
||||||
this.togglePushToken(true);
|
|
||||||
} else {
|
|
||||||
this.tinode.setDeviceToken(this.state.firebaseToken);
|
this.tinode.setDeviceToken(this.state.firebaseToken);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// do nothing: handled earlier.
|
// do nothing: handled earlier.
|
||||||
// catch needed to pervent unnecessary logging of error.
|
// catch needed to pervent unnecessary logging of error.
|
||||||
@@ -317,11 +315,22 @@ class TinodeWeb extends React.Component {
|
|||||||
return tinode;
|
return tinode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notifiy Tinode that a push message was received from the server.
|
// Tinode received a push notification from the server.
|
||||||
handlePushMessage(data) {
|
handlePushMessage(payload) {
|
||||||
if (data.what == 'msg' && Tinode.isChannelTopicName(data.topic)) {
|
const data = payload.data || {};
|
||||||
|
switch (data.what) {
|
||||||
|
case 'msg':
|
||||||
|
if (Tinode.isChannelTopicName(data.topic)) {
|
||||||
// The last argument is a fake user Id: otherwise the update is seen as one from the current user.
|
// The last argument is a fake user Id: otherwise the update is seen as one from the current user.
|
||||||
this.tinode.oobNotification(data.topic, data.seq, 'fake-uid');
|
this.tinode.oobNotification('msg', data.topic, data.seq, 'fake-uid');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'read':
|
||||||
|
// Message read on another device, clear it here.
|
||||||
|
this.tinode.oobNotification('read', data.topic, data.seq);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn("Unknown push type ignored", data.what, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,40 +339,77 @@ class TinodeWeb extends React.Component {
|
|||||||
const onError = (msg, err) => {
|
const onError = (msg, err) => {
|
||||||
console.error(msg, err);
|
console.error(msg, err);
|
||||||
this.handleError(formatMessage(messages.push_init_failed), 'err');
|
this.handleError(formatMessage(messages.push_init_failed), 'err');
|
||||||
this.setState({desktopAlertsEnabled: false});
|
this.setState({desktopAlertsEnabled: false, firebaseToken: null});
|
||||||
|
LocalStorageUtil.updateObject('settings', {desktopAlerts: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.fbPush = firebase.initializeApp(FIREBASE_INIT, APP_NAME).messaging();
|
this.fcm = firebaseGetMessaging(firebaseInitApp(FIREBASE_INIT, APP_NAME));
|
||||||
this.fbPush.usePublicVapidKey(FIREBASE_INIT.messagingVapidKey);
|
|
||||||
|
|
||||||
return navigator.serviceWorker.register('/service-worker.js').then((reg) => {
|
return navigator.serviceWorker.register('/service-worker.js').then((reg) => {
|
||||||
this.checkForAppUpdate(reg);
|
this.checkForAppUpdate(reg);
|
||||||
this.fbPush.useServiceWorker(reg);
|
|
||||||
reg.active.postMessage(JSON.stringify({locale: locale, version: PACKAGE_VERSION}));
|
reg.active.postMessage(JSON.stringify({locale: locale, version: PACKAGE_VERSION}));
|
||||||
|
return reg;
|
||||||
// Google could not be bothered to mention that onTokenRefresh is never called.
|
}).then((reg) => {
|
||||||
this.fbPush.onTokenRefresh(() => {
|
// Request token.
|
||||||
this.requestPushToken();
|
return TinodeWeb.requestFCMToken(this.fcm, reg);
|
||||||
});
|
}).then((token) => {
|
||||||
|
const persist = LocalStorageUtil.getObject('keep-logged-in');
|
||||||
|
if (token != this.state.firebaseToken) {
|
||||||
|
this.tinode.setDeviceToken(token);
|
||||||
|
if (persist) {
|
||||||
|
LocalStorageUtil.setObject('firebase-token', token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setState({firebaseToken: token, desktopAlerts: true});
|
||||||
|
if (persist) {
|
||||||
|
LocalStorageUtil.updateObject('settings', {desktopAlerts: true});
|
||||||
|
}
|
||||||
|
|
||||||
// Handhe FCM pushes
|
// Handhe FCM pushes
|
||||||
// (a) for channels always,
|
// (a) for channels always,
|
||||||
// (b) pushes when the app is in foreground but has not focus.
|
// (b) pushes when the app is in foreground but has no focus.
|
||||||
this.fbPush.onMessage(payload => { this.handlePushMessage(payload.data); });
|
firebaseOnMessage(this.fcm, (payload) => { this.handlePushMessage(payload); });
|
||||||
|
|
||||||
return reg;
|
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
// registration failed :(
|
// SW registration or FCM has failed :(
|
||||||
onError("Failed to register service worker:", err);
|
onError(err);
|
||||||
return Promise.reject(err);
|
throw err;
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
onError("Failed to initialize push notifications", err);
|
onError(err);
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Google's FCM API is idiotic.
|
||||||
|
static requestFCMToken(fcm, sw) {
|
||||||
|
return firebaseGetToken(fcm, {
|
||||||
|
serviceWorkerRegistration: sw,
|
||||||
|
vapidKey: FIREBASE_INIT.messagingVapidKey
|
||||||
|
}).then((token) => {
|
||||||
|
if (token) {
|
||||||
|
return token;
|
||||||
|
} else {
|
||||||
|
// Try to request permissions.
|
||||||
|
return Notification.requestPermission().then((permission) => {
|
||||||
|
if (permission === 'granted') {
|
||||||
|
return firebaseGetToken(fcm, {
|
||||||
|
serviceWorkerRegistration: reg,
|
||||||
|
vapidKey: FIREBASE_INIT.messagingVapidKey
|
||||||
|
}).then((token) => {
|
||||||
|
if (token) {
|
||||||
|
return token;
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to initialize notifications");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("No permission to send notifications: " + permission);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
handleResize() {
|
handleResize() {
|
||||||
const mobile = document.documentElement.clientWidth <= MEDIA_BREAKPOINT;
|
const mobile = document.documentElement.clientWidth <= MEDIA_BREAKPOINT;
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -414,14 +460,10 @@ class TinodeWeb extends React.Component {
|
|||||||
// Clear invalid topic name.
|
// Clear invalid topic name.
|
||||||
topicName = null;
|
topicName = null;
|
||||||
}
|
}
|
||||||
const newState = {
|
this.setState({
|
||||||
topicSelected: topicName
|
topicSelected: topicName,
|
||||||
};
|
topicSelectedAcs: this.tinode.getTopicAccessMode(topicName)
|
||||||
const acs = this.tinode.getTopicAccessMode(topicName);
|
});
|
||||||
if (acs) {
|
|
||||||
newState.topicSelectedAcs = acs;
|
|
||||||
}
|
|
||||||
this.setState(newState);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Empty hashpath
|
// Empty hashpath
|
||||||
@@ -865,7 +907,7 @@ class TinodeWeb extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// User clicked on a contact in the side panel or deleted a contact.
|
// User clicked on a topic in the side panel or deleted a topic.
|
||||||
handleTopicSelected(topicName) {
|
handleTopicSelected(topicName) {
|
||||||
// Clear newTopicParams after use.
|
// Clear newTopicParams after use.
|
||||||
if (this.state.newTopicParams && this.state.newTopicParams._topicName != topicName) {
|
if (this.state.newTopicParams && this.state.newTopicParams._topicName != topicName) {
|
||||||
@@ -881,7 +923,7 @@ class TinodeWeb extends React.Component {
|
|||||||
mobilePanel: 'topic-view',
|
mobilePanel: 'topic-view',
|
||||||
infoPanel: undefined
|
infoPanel: undefined
|
||||||
});
|
});
|
||||||
// Different contact selected.
|
// Different topic selected.
|
||||||
if (this.state.topicSelected != topicName) {
|
if (this.state.topicSelected != topicName) {
|
||||||
this.setState({
|
this.setState({
|
||||||
topicSelectedOnline: this.tinode.isTopicOnline(topicName),
|
topicSelectedOnline: this.tinode.isTopicOnline(topicName),
|
||||||
@@ -945,7 +987,7 @@ class TinodeWeb extends React.Component {
|
|||||||
// If there are unsent messages, try sending them now.
|
// If there are unsent messages, try sending them now.
|
||||||
topic.queuedMessages((pub) => {
|
topic.queuedMessages((pub) => {
|
||||||
if (!pub._sending && topic.isSubscribed()) {
|
if (!pub._sending && topic.isSubscribed()) {
|
||||||
this.sendMessage(pub);
|
topic.publishMessage(pub);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1101,31 +1143,19 @@ class TinodeWeb extends React.Component {
|
|||||||
this.state.myUserId ? 'blocked' : ''));
|
this.state.myUserId ? 'blocked' : ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
togglePushToken(enabled) {
|
toggleFCMToken(enabled) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
this.setState({desktopAlerts: null});
|
this.setState({desktopAlerts: null});
|
||||||
if (!this.state.firebaseToken) {
|
if (!this.state.firebaseToken) {
|
||||||
const fcm = this.fbPush ?
|
|
||||||
Promise.resolve() :
|
|
||||||
this.initFCMessaging();
|
this.initFCMessaging();
|
||||||
fcm.then(() => {
|
|
||||||
return this.fbPush.requestPermission();
|
|
||||||
}).then(() => {
|
|
||||||
this.requestPushToken();
|
|
||||||
}).catch((err) => {
|
|
||||||
console.error("Failed to get notification permission.", err);
|
|
||||||
this.handleError(err.message, 'err');
|
|
||||||
this.setState({desktopAlerts: false, firebaseToken: null});
|
|
||||||
LocalStorageUtil.updateObject('settings', {desktopAlerts: false});
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.setState({desktopAlerts: true});
|
this.setState({desktopAlerts: true});
|
||||||
if (LocalStorageUtil.getObject('keep-logged-in')) {
|
if (LocalStorageUtil.getObject('keep-logged-in')) {
|
||||||
LocalStorageUtil.updateObject('settings', {desktopAlerts: true});
|
LocalStorageUtil.updateObject('settings', {desktopAlerts: true});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (this.state.firebaseToken && this.fbPush) {
|
} else if (this.state.firebaseToken && this.fcm) {
|
||||||
this.fbPush.deleteToken(this.state.firebaseToken).catch((err) => {
|
firebaseDelToken(this.fcm).catch((err) => {
|
||||||
console.error("Unable to delete token.", err);
|
console.error("Unable to delete token.", err);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
LocalStorageUtil.updateObject('settings', {desktopAlerts: false});
|
LocalStorageUtil.updateObject('settings', {desktopAlerts: false});
|
||||||
@@ -1140,25 +1170,6 @@ class TinodeWeb extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
requestPushToken() {
|
|
||||||
this.fbPush.getToken().then((refreshedToken) => {
|
|
||||||
const persist = LocalStorageUtil.getObject('keep-logged-in');
|
|
||||||
if (refreshedToken != this.state.firebaseToken) {
|
|
||||||
this.tinode.setDeviceToken(refreshedToken);
|
|
||||||
if (persist) {
|
|
||||||
LocalStorageUtil.setObject('firebase-token', refreshedToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.setState({firebaseToken: refreshedToken, desktopAlerts: true});
|
|
||||||
if (persist) {
|
|
||||||
LocalStorageUtil.updateObject('settings', {desktopAlerts: true});
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
|
||||||
this.handleError(err.message, 'err');
|
|
||||||
console.error("Failed to retrieve firebase token", err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleToggleMessageSounds(enabled) {
|
handleToggleMessageSounds(enabled) {
|
||||||
this.setState({messageSounds: enabled});
|
this.setState({messageSounds: enabled});
|
||||||
LocalStorageUtil.updateObject('settings', {
|
LocalStorageUtil.updateObject('settings', {
|
||||||
@@ -1338,24 +1349,30 @@ class TinodeWeb extends React.Component {
|
|||||||
localStorage.removeItem('firebase-token');
|
localStorage.removeItem('firebase-token');
|
||||||
localStorage.removeItem('settings');
|
localStorage.removeItem('settings');
|
||||||
if (this.state.firebaseToken) {
|
if (this.state.firebaseToken) {
|
||||||
this.fbPush.deleteToken(this.state.firebaseToken)
|
firebaseDelToken(this.fcm);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearInterval(this.reconnectCountdown);
|
clearInterval(this.reconnectCountdown);
|
||||||
|
|
||||||
|
let cleared;
|
||||||
if (this.tinode) {
|
if (this.tinode) {
|
||||||
this.tinode.clearStorage();
|
cleared = this.tinode.clearStorage();
|
||||||
this.tinode.onDisconnect = undefined;
|
this.tinode.onDisconnect = undefined;
|
||||||
this.tinode.disconnect();
|
this.tinode.disconnect();
|
||||||
|
} else {
|
||||||
|
cleared = Promose.resolve();
|
||||||
}
|
}
|
||||||
this.setState(this.getBlankState());
|
this.setState(this.getBlankState());
|
||||||
|
|
||||||
|
cleared.then(() => {
|
||||||
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress,
|
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress,
|
||||||
this.state.transport, this.props.intl.locale, LocalStorageUtil.getObject('keep-logged-in'));
|
this.state.transport, this.props.intl.locale, LocalStorageUtil.getObject('keep-logged-in'), () => {
|
||||||
this.tinode.onConnect = this.handleConnected;
|
this.tinode.onConnect = this.handleConnected;
|
||||||
this.tinode.onDisconnect = this.handleDisconnect;
|
this.tinode.onDisconnect = this.handleDisconnect;
|
||||||
this.tinode.onAutoreconnectIteration = this.handleAutoreconnectIteration;
|
this.tinode.onAutoreconnectIteration = this.handleAutoreconnectIteration;
|
||||||
HashNavigation.navigateTo('');
|
HashNavigation.navigateTo('');
|
||||||
|
})
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteAccount() {
|
handleDeleteAccount() {
|
||||||
@@ -1426,7 +1443,7 @@ class TinodeWeb extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Publish spam report.
|
// Publish spam report.
|
||||||
this.tinode.publish(Tinode.TOPIC_SYS, Tinode.Drafty.attachJSON(null, {
|
this.tinode.publish(Tinode.TOPIC_SYS, Drafty.attachJSON(null, {
|
||||||
'action': 'report',
|
'action': 'report',
|
||||||
'target': topicName
|
'target': topicName
|
||||||
}));
|
}));
|
||||||
@@ -1457,16 +1474,11 @@ class TinodeWeb extends React.Component {
|
|||||||
this.handleSidepanelCancel();
|
this.handleSidepanelCancel();
|
||||||
}
|
}
|
||||||
const header = '➦ ' + params.userName;
|
const header = '➦ ' + params.userName;
|
||||||
const content = typeof params.content == 'string' ?
|
const content = typeof params.content == 'string' ? Drafty.init(params.content) : Drafty.forwardedContent(params.content);
|
||||||
Tinode.Drafty.init(params.content) : Tinode.Drafty.forwardedContent(params.content);
|
const preview = Drafty.preview(content, FORWARDED_PREVIEW_LENGTH, undefined, params.forwarded != null);
|
||||||
const preview = Tinode.Drafty.preview(content, FORWARDED_PREVIEW_LENGTH,
|
const msg = Drafty.append(Drafty.appendLineBreak(Drafty.mention(header, params.userFrom)), content);
|
||||||
undefined, params.forwarded != null);
|
|
||||||
const msg = Tinode.Drafty.append(
|
|
||||||
Tinode.Drafty.appendLineBreak(
|
|
||||||
Tinode.Drafty.mention(header, params.userFrom)),
|
|
||||||
content);
|
|
||||||
|
|
||||||
const msgPreview = Tinode.Drafty.quote(header, params.userFrom, preview);
|
const msgPreview = Drafty.quote(header, params.userFrom, preview);
|
||||||
const head = {
|
const head = {
|
||||||
forwarded: params.topicName + ':' + params.seq
|
forwarded: params.topicName + ':' + params.seq
|
||||||
};
|
};
|
||||||
@@ -1718,7 +1730,7 @@ class TinodeWeb extends React.Component {
|
|||||||
onUpdateAccountDesc={this.handleTopicUpdateRequest}
|
onUpdateAccountDesc={this.handleTopicUpdateRequest}
|
||||||
onUpdatePassword={this.handleUpdatePasswordRequest}
|
onUpdatePassword={this.handleUpdatePasswordRequest}
|
||||||
onUpdateAccountTags={this.handleUpdateAccountTagsRequest}
|
onUpdateAccountTags={this.handleUpdateAccountTagsRequest}
|
||||||
onTogglePushNotifications={this.togglePushToken}
|
onTogglePushNotifications={this.toggleFCMToken}
|
||||||
onToggleMessageSounds={this.handleToggleMessageSounds}
|
onToggleMessageSounds={this.handleToggleMessageSounds}
|
||||||
onToggleIncognitoMode={this.handleToggleIncognitoMode}
|
onToggleIncognitoMode={this.handleToggleIncognitoMode}
|
||||||
onCredAdd={this.handleCredAdd}
|
onCredAdd={this.handleCredAdd}
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
// Audio recorder widget.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { secondsToTime } from '../lib/strformat';
|
||||||
|
|
||||||
|
// FFT resolution.
|
||||||
|
const BUFFER_SIZE = 256;
|
||||||
|
// Thickness of a visualization bar.
|
||||||
|
const LINE_WIDTH = 4;
|
||||||
|
// Spacing between two visualization bars.
|
||||||
|
const SPACING = 2;
|
||||||
|
// Duration represented by one visualization bar.
|
||||||
|
const MILLIS_PER_BAR = 100;
|
||||||
|
// Color of histogram bars
|
||||||
|
const BAR_COLOR = '#8fbed6';
|
||||||
|
// Scaling for visualization bars.
|
||||||
|
const BAR_SCALE = 96.0;
|
||||||
|
// Background color
|
||||||
|
const BKG_COLOR = '#eeeeee';
|
||||||
|
// Minimum duration of a recording in milliseconds.
|
||||||
|
const MIN_DURATION = 200;
|
||||||
|
// Maximum duration of a recording in milliseconds (10 min).
|
||||||
|
const MAX_DURATION = 600000;
|
||||||
|
|
||||||
|
export default class AudioRecorder extends React.PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
enabled: true,
|
||||||
|
audioRecord: null,
|
||||||
|
recording: true,
|
||||||
|
paused: false,
|
||||||
|
duration: '0:00'
|
||||||
|
};
|
||||||
|
|
||||||
|
this.visualize = this.visualize.bind(this);
|
||||||
|
this.initMediaRecording = this.initMediaRecording.bind(this);
|
||||||
|
this.cleanUp = this.cleanUp.bind(this);
|
||||||
|
|
||||||
|
this.handleResume = this.handleResume.bind(this);
|
||||||
|
this.handlePause = this.handlePause.bind(this);
|
||||||
|
this.handleDelete = this.handleDelete.bind(this);
|
||||||
|
this.handleDone = this.handleDone.bind(this);
|
||||||
|
|
||||||
|
this.durationMillis = 0;
|
||||||
|
this.startedOn = null;
|
||||||
|
this.viewBuffer = [];
|
||||||
|
this.canvasRef = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.canvasContext = this.canvasRef.current.getContext('2d');
|
||||||
|
this.canvasContext.lineCap = 'round';
|
||||||
|
// To reduce line blurring.
|
||||||
|
this.canvasContext.translate(0.5, 0.5);
|
||||||
|
|
||||||
|
this.stream = null;
|
||||||
|
this.mediaRecorder = null;
|
||||||
|
this.audioContext = null;
|
||||||
|
this.audioInput = null;
|
||||||
|
this.analyser = null;
|
||||||
|
|
||||||
|
this.audioChunks = [];
|
||||||
|
|
||||||
|
// Start recorder right away.
|
||||||
|
try {
|
||||||
|
navigator.mediaDevices.getUserMedia({audio: true, video: false}).then(this.initMediaRecording, this.props.onError);
|
||||||
|
} catch (err) {
|
||||||
|
this.props.onError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw amplitude of sound.
|
||||||
|
visualize() {
|
||||||
|
const pcmData = new Uint8Array(this.analyser.frequencyBinCount);
|
||||||
|
const width = this.canvasRef.current.width;
|
||||||
|
const height = this.canvasRef.current.height;
|
||||||
|
const viewLength = (width / (LINE_WIDTH + SPACING)) | 0;
|
||||||
|
const viewDuration = MILLIS_PER_BAR * viewLength;
|
||||||
|
|
||||||
|
this.canvasContext.fillStyle = BKG_COLOR;
|
||||||
|
this.canvasContext.lineWidth = LINE_WIDTH;
|
||||||
|
this.canvasContext.strokeStyle = BAR_COLOR;
|
||||||
|
|
||||||
|
let prevBarCount = 0;
|
||||||
|
const drawFrame = () => {
|
||||||
|
if (!this.startedOn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.requestAnimationFrame(drawFrame);
|
||||||
|
|
||||||
|
const duration = this.durationMillis + (Date.now() - this.startedOn);
|
||||||
|
// Update record length timer.
|
||||||
|
this.setState({duration: secondsToTime(duration / 1000)});
|
||||||
|
|
||||||
|
// Check if record is too long.
|
||||||
|
if (duration > MAX_DURATION) {
|
||||||
|
this.mediaRecorder.pause();
|
||||||
|
this.durationMillis += Date.now() - this.startedOn;
|
||||||
|
this.startedOn = null;
|
||||||
|
this.setState({enabled: false, recording: false, duration: secondsToTime(this.durationMillis / 1000)});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw histogram.
|
||||||
|
|
||||||
|
// Get current waveform and calculate its amplitude.
|
||||||
|
this.analyser.getByteTimeDomainData(pcmData);
|
||||||
|
let volume = 0.0;
|
||||||
|
for (const amplitude of pcmData) {
|
||||||
|
volume += (amplitude - 127) ** 2;
|
||||||
|
}
|
||||||
|
volume = Math.sqrt(volume/pcmData.length);
|
||||||
|
|
||||||
|
let barCount = (duration / MILLIS_PER_BAR) | 0;
|
||||||
|
// Shift of the histogram along x-axis to make scrolling smooth. No need to shift if recording is too short.
|
||||||
|
const dx = viewDuration > duration ? 0 :
|
||||||
|
(duration - MILLIS_PER_BAR * barCount) / MILLIS_PER_BAR * (LINE_WIDTH + SPACING);
|
||||||
|
|
||||||
|
if (prevBarCount != barCount) {
|
||||||
|
prevBarCount = barCount;
|
||||||
|
// Add new amplitude visualization bar.
|
||||||
|
this.viewBuffer.push(volume);
|
||||||
|
if (this.viewBuffer.length > viewLength) {
|
||||||
|
// Keep at most 'viewLength' amplitude bars.
|
||||||
|
this.viewBuffer.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear canvas.
|
||||||
|
this.canvasContext.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
// Draw amplitude bars.
|
||||||
|
this.canvasContext.beginPath();
|
||||||
|
for (let i = 0; i < this.viewBuffer.length; i++) {
|
||||||
|
let x = i * (LINE_WIDTH + SPACING) - dx;
|
||||||
|
let y = this.viewBuffer[i] / BAR_SCALE * height;
|
||||||
|
|
||||||
|
this.canvasContext.moveTo(x, (height - y) * 0.5);
|
||||||
|
this.canvasContext.lineTo(x, height * 0.5 + y * 0.5);
|
||||||
|
}
|
||||||
|
// Actually draw the bars on canvas.
|
||||||
|
this.canvasContext.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePause(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.mediaRecorder.pause();
|
||||||
|
this.durationMillis += Date.now() - this.startedOn;
|
||||||
|
this.startedOn = null;
|
||||||
|
this.setState({recording: false, duration: secondsToTime(this.durationMillis / 1000)});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResume(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.state.enabled) {
|
||||||
|
this.startedOn = Date.now();
|
||||||
|
this.mediaRecorder.resume();
|
||||||
|
this.setState({recording: true}, this.visualize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDelete(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.durationMillis = 0;
|
||||||
|
this.startedOn = null;
|
||||||
|
this.mediaRecorder.stop();
|
||||||
|
this.cleanUp();
|
||||||
|
this.setState({recording: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDone(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.setState({recording: false});
|
||||||
|
if (this.startedOn) {
|
||||||
|
this.durationMillis += Date.now() - this.startedOn;
|
||||||
|
this.startedOn = null;
|
||||||
|
}
|
||||||
|
// Stop recording and return data.
|
||||||
|
if (this.mediaRecorder) {
|
||||||
|
this.mediaRecorder.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initMediaRecording(stream) {
|
||||||
|
this.stream = stream;
|
||||||
|
this.mediaRecorder = new MediaRecorder(stream);
|
||||||
|
|
||||||
|
// The following code is needed for visualization.
|
||||||
|
this.audioContext = new AudioContext();
|
||||||
|
this.audioInput = this.audioContext.createMediaStreamSource(stream);
|
||||||
|
this.analyser = this.audioContext.createAnalyser();
|
||||||
|
this.analyser.fftSize = BUFFER_SIZE;
|
||||||
|
this.audioInput.connect(this.analyser);
|
||||||
|
|
||||||
|
this.mediaRecorder.onstop = _ => {
|
||||||
|
const blob = new Blob(this.audioChunks, { 'type' : 'audio/mp3; codecs=mp3' });
|
||||||
|
this.audioChunks = [];
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
this.cleanUp();
|
||||||
|
if (this.durationMillis > MIN_DURATION) {
|
||||||
|
this.props.onFinished(url, this.durationMillis);
|
||||||
|
} else {
|
||||||
|
this.props.onDeleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mediaRecorder.ondataavailable = (e) => {
|
||||||
|
if (e.data.size > 0) {
|
||||||
|
this.audioChunks.push(e.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.durationMillis = 0;
|
||||||
|
this.startedOn = Date.now();
|
||||||
|
this.mediaRecorder.start();
|
||||||
|
this.visualize();
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanUp() {
|
||||||
|
this.audioInput.disconnect();
|
||||||
|
this.stream.getTracks().forEach(track => track.stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const resumeClass = 'material-icons ' + (this.state.enabled ? 'red' : 'gray');
|
||||||
|
return (
|
||||||
|
<div className="audio">
|
||||||
|
<a href="#" onClick={this.handleDelete} title="Delete">
|
||||||
|
<i className="material-icons">delete_outline</i>
|
||||||
|
</a>
|
||||||
|
<canvas className="visualiser" width="200" height="60" ref={this.canvasRef} />
|
||||||
|
<div className="duration">{this.state.duration}</div>
|
||||||
|
{this.state.recording ?
|
||||||
|
<a href="#" onClick={this.handlePause} title="Pause">
|
||||||
|
<i className="material-icons">pause_circle_outline</i>
|
||||||
|
</a> :
|
||||||
|
<a href="#" onClick={this.handleResume} title="Resume">
|
||||||
|
<i className={resumeClass}>radio_button_checked</i>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
<a href="#" onClick={this.handleDone} title="Send">
|
||||||
|
<i className="material-icons">send</i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import { Drafty } from 'tinode-sdk';
|
import { Drafty, Tinode } from 'tinode-sdk';
|
||||||
import Tinode from 'tinode-sdk';
|
|
||||||
|
|
||||||
import Contact from './contact.jsx';
|
import Contact from './contact.jsx';
|
||||||
import ContactAction from './contact-action.jsx';
|
import ContactAction from './contact-action.jsx';
|
||||||
@@ -115,7 +114,7 @@ class ContactList extends React.Component {
|
|||||||
isVerified={c.trusted && c.trusted.verified}
|
isVerified={c.trusted && c.trusted.verified}
|
||||||
isStaff={c.trusted && c.trusted.staff}
|
isStaff={c.trusted && c.trusted.staff}
|
||||||
isDangerous={c.trusted && c.trusted.danger}
|
isDangerous={c.trusted && c.trusted.danger}
|
||||||
deleted={c.deleted}
|
deleted={c._deleted}
|
||||||
item={key}
|
item={key}
|
||||||
index={contactNodes.length}
|
index={contactNodes.length}
|
||||||
key={key} />
|
key={key} />
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ class Contact extends React.Component {
|
|||||||
const icon = deliveryMarker(this.props.received);
|
const icon = deliveryMarker(this.props.received);
|
||||||
const marker = icon ? <i className={'material-icons small space-right' +
|
const marker = icon ? <i className={'material-icons small space-right' +
|
||||||
(icon.color ? ' ' + icon.color : '')}>{icon.name}</i> : null;
|
(icon.color ? ' ' + icon.color : '')}>{icon.name}</i> : null;
|
||||||
|
const titleClass = 'contact-title' + (this.props.deleted ? ' deleted' : '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={!this.props.showCheckmark && this.props.selected ? 'selected' : null} onClick={this.handleClick}>
|
<li className={!this.props.showCheckmark && this.props.selected ? 'selected' : null} onClick={this.handleClick}>
|
||||||
@@ -95,7 +96,7 @@ class Contact extends React.Component {
|
|||||||
avatar={avatar}
|
avatar={avatar}
|
||||||
title={this.props.title}
|
title={this.props.title}
|
||||||
topic={this.props.item}
|
topic={this.props.item}
|
||||||
disabled={this.props.deleted} />
|
deleted={this.props.deleted} />
|
||||||
{this.props.deleted ? <i className="deleted material-icons">cancel</i> :
|
{this.props.deleted ? <i className="deleted material-icons">cancel</i> :
|
||||||
this.props.showOnline ? <span className={online} /> :
|
this.props.showOnline ? <span className={online} /> :
|
||||||
(this.props.showCheckmark && this.props.selected) ?
|
(this.props.showCheckmark && this.props.selected) ?
|
||||||
@@ -103,9 +104,10 @@ class Contact extends React.Component {
|
|||||||
: null}
|
: null}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-box">
|
<div className="text-box">
|
||||||
<div><span className="contact-title">{title}</span>
|
<div><span className={titleClass}>{title}</span>
|
||||||
{this.props.isChannel ? <img src="/img/channel.png" className="channel" alt="channel" /> : null}
|
{this.props.isChannel ? <img src="/img/channel.png" className="channel" alt="channel" /> : null}
|
||||||
<ContactBadges badges={icon_badges} /><UnreadBadge count={this.props.unread} />
|
<ContactBadges badges={icon_badges} />
|
||||||
|
{!this.props.deleted ? <UnreadBadge count={this.props.unread} /> : null}
|
||||||
</div>
|
</div>
|
||||||
{this.props.showMode ?
|
{this.props.showMode ?
|
||||||
<span><ContactBadges badges={badges} /></span> :
|
<span><ContactBadges badges={badges} /></span> :
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ class ContextMenu extends React.Component {
|
|||||||
(() => {
|
(() => {
|
||||||
const topic = this.props.tinode.getTopic(params.topicName);
|
const topic = this.props.tinode.getTopic(params.topicName);
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
console.log("Topic not found: ", params.topicName);
|
console.warn("Topic not found: ", params.topicName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return topic.delTopic(true).catch((err) => {
|
return topic.delTopic(true).catch((err) => {
|
||||||
@@ -258,7 +258,7 @@ class ContextMenu extends React.Component {
|
|||||||
handler: (params, errorHandler) => {
|
handler: (params, errorHandler) => {
|
||||||
const topic = this.props.tinode.getTopic(params.topicName);
|
const topic = this.props.tinode.getTopic(params.topicName);
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
console.log("Topic not found: ", params.topicName);
|
console.warn("Topic not found: ", params.topicName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return topic.archive(true).catch((err) => {
|
return topic.archive(true).catch((err) => {
|
||||||
@@ -274,7 +274,7 @@ class ContextMenu extends React.Component {
|
|||||||
handler: (params, errorHandler) => {
|
handler: (params, errorHandler) => {
|
||||||
const topic = this.props.tinode.getTopic(params.topicName);
|
const topic = this.props.tinode.getTopic(params.topicName);
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
console.log("Topic not found: ", params.topicName);
|
console.warn("Topic not found: ", params.topicName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return topic.archive(false).catch((err) => {
|
return topic.archive(false).catch((err) => {
|
||||||
@@ -296,7 +296,7 @@ class ContextMenu extends React.Component {
|
|||||||
handler: (params, errorHandler) => {
|
handler: (params, errorHandler) => {
|
||||||
const topic = this.props.tinode.getTopic(params.topicName);
|
const topic = this.props.tinode.getTopic(params.topicName);
|
||||||
if (!topic || !params.user) {
|
if (!topic || !params.user) {
|
||||||
console.log("Topic or user not found: '" + params.topicName + "', '" + params.user + "'");
|
console.warn("Topic or user not found: '" + params.topicName + "', '" + params.user + "'");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return topic.delSubscription(params.user).catch((err) => {
|
return topic.delSubscription(params.user).catch((err) => {
|
||||||
@@ -378,7 +378,7 @@ class ContextMenu extends React.Component {
|
|||||||
deleteMessages(all, hard, params, errorHandler) {
|
deleteMessages(all, hard, params, errorHandler) {
|
||||||
const topic = this.props.tinode.getTopic(params.topicName);
|
const topic = this.props.tinode.getTopic(params.topicName);
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
console.log("Topic not found: ", params.topicName);
|
console.warn("Topic not found: ", params.topicName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,7 +420,7 @@ class ContextMenu extends React.Component {
|
|||||||
topicPermissionSetter(mode, params, errorHandler) {
|
topicPermissionSetter(mode, params, errorHandler) {
|
||||||
const topic = this.props.tinode.getTopic(params.topicName);
|
const topic = this.props.tinode.getTopic(params.topicName);
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
console.log("Topic not found", params.topicName);
|
console.warn("Topic not found", params.topicName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import Tinode from 'tinode-sdk';
|
import { Tinode } from 'tinode-sdk';
|
||||||
|
|
||||||
import ContactList from './contact-list.jsx';
|
import ContactList from './contact-list.jsx';
|
||||||
import SearchContacts from './search-contacts.jsx';
|
import SearchContacts from './search-contacts.jsx';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Tinode from 'tinode-sdk';
|
import { Tinode } from 'tinode-sdk';
|
||||||
|
|
||||||
import { idToColorClass } from '../lib/strformat.js';
|
import { idToColorClass } from '../lib/strformat.js';
|
||||||
import { sanitizeImageUrl } from '../lib/utils.js';
|
import { sanitizeImageUrl } from '../lib/utils.js';
|
||||||
@@ -13,16 +13,16 @@ export default class LetterTile extends React.PureComponent {
|
|||||||
const iconColor = idToColorClass(this.props.topic, isGroup);
|
const iconColor = idToColorClass(this.props.topic, isGroup);
|
||||||
if (this.props.topic && this.props.title && this.props.title.trim()) {
|
if (this.props.topic && this.props.title && this.props.title.trim()) {
|
||||||
const letter = this.props.title.trim().charAt(0);
|
const letter = this.props.title.trim().charAt(0);
|
||||||
const className = 'lettertile ' + iconColor + (this.props.disabled ? ' disabled' : '');
|
const className = 'lettertile ' + iconColor + (this.props.deleted ? ' disabled' : '');
|
||||||
avatar = (<div className={className}><div>{letter}</div></div>)
|
avatar = (<div className={className}><div>{letter}</div></div>)
|
||||||
} else {
|
} else {
|
||||||
const className = 'material-icons ' + iconColor + (this.props.disabled ? ' disabled' : '');
|
const className = 'material-icons ' + iconColor + (this.props.deleted ? ' disabled' : '');
|
||||||
avatar = isGroup ? <i className={className}>group</i> : <i className={className}>person</i>;
|
avatar = isGroup ? <i className={className}>group</i> : <i className={className}>person</i>;
|
||||||
}
|
}
|
||||||
} else if (this.props.avatar) {
|
} else if (this.props.avatar) {
|
||||||
const url = this.props.tinode.authorizeURL(sanitizeImageUrl(this.props.avatar));
|
const url = this.props.tinode.authorizeURL(sanitizeImageUrl(this.props.avatar));
|
||||||
// If avatar image is invalid, show a placeholder.
|
// If avatar image is invalid, show a placeholder.
|
||||||
const className = 'avatar' + (this.props.disabled ? ' disabled' : '');
|
const className = 'avatar' + (this.props.deleted ? ' deleted' : '');
|
||||||
avatar = <img className={className} alt="avatar" src={url}
|
avatar = <img className={className} alt="avatar" src={url}
|
||||||
onError={(e)=>{e.target.onerror = null; e.target.src="../img/broken_image.png"}} />;
|
onError={(e)=>{e.target.onerror = null; e.target.src="../img/broken_image.png"}} />;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import Tinode from 'tinode-sdk';
|
import { Tinode } from 'tinode-sdk';
|
||||||
|
|
||||||
import { deliveryMarker } from '../lib/utils.js';
|
import { deliveryMarker } from '../lib/utils.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Tinode from 'tinode-sdk';
|
import { Tinode } from 'tinode-sdk';
|
||||||
|
|
||||||
export default class SearchContacts extends React.PureComponent {
|
export default class SearchContacts extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import React from 'react';
|
|||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import { Drafty } from 'tinode-sdk';
|
import { Drafty } from 'tinode-sdk';
|
||||||
|
|
||||||
|
import AudioRecorder from './audio-recorder.jsx';
|
||||||
|
|
||||||
import { KEYPRESS_DELAY } from '../config.js';
|
import { KEYPRESS_DELAY } from '../config.js';
|
||||||
import { filePasted } from '../lib/blob-helpers.js';
|
import { filePasted } from '../lib/blob-helpers.js';
|
||||||
import { replyFormatter } from '../lib/formatters.js';
|
import { replyFormatter } from '../lib/formatters.js';
|
||||||
@@ -42,6 +44,8 @@ class SendMessage extends React.PureComponent {
|
|||||||
this.state = {
|
this.state = {
|
||||||
quote: null,
|
quote: null,
|
||||||
message: '',
|
message: '',
|
||||||
|
audioRec: false,
|
||||||
|
audioEnabled: !!navigator.mediaDevices.getUserMedia,
|
||||||
// Make initial keypress time as if it happened 5001 milliseconds in the past.
|
// Make initial keypress time as if it happened 5001 milliseconds in the past.
|
||||||
keypressTimestamp: new Date().getTime() - KEYPRESS_DELAY - 1
|
keypressTimestamp: new Date().getTime() - KEYPRESS_DELAY - 1
|
||||||
};
|
};
|
||||||
@@ -49,6 +53,7 @@ class SendMessage extends React.PureComponent {
|
|||||||
this.handlePasteEvent = this.handlePasteEvent.bind(this);
|
this.handlePasteEvent = this.handlePasteEvent.bind(this);
|
||||||
this.handleAttachImage = this.handleAttachImage.bind(this);
|
this.handleAttachImage = this.handleAttachImage.bind(this);
|
||||||
this.handleAttachFile = this.handleAttachFile.bind(this);
|
this.handleAttachFile = this.handleAttachFile.bind(this);
|
||||||
|
this.handleAttachAudio = this.handleAttachAudio.bind(this);
|
||||||
this.handleSend = this.handleSend.bind(this);
|
this.handleSend = this.handleSend.bind(this);
|
||||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
this.handleKeyPress = this.handleKeyPress.bind(this);
|
||||||
this.handleMessageTyping = this.handleMessageTyping.bind(this);
|
this.handleMessageTyping = this.handleMessageTyping.bind(this);
|
||||||
@@ -118,7 +123,6 @@ class SendMessage extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleAttachFile(e) {
|
handleAttachFile(e) {
|
||||||
const {formatMessage} = this.props.intl;
|
|
||||||
if (e.target.files && e.target.files.length > 0) {
|
if (e.target.files && e.target.files.length > 0) {
|
||||||
this.props.onAttachFile(e.target.files[0]);
|
this.props.onAttachFile(e.target.files[0]);
|
||||||
}
|
}
|
||||||
@@ -126,6 +130,11 @@ class SendMessage extends React.PureComponent {
|
|||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleAttachAudio(url, duration) {
|
||||||
|
this.setState({audioRec: false});
|
||||||
|
this.props.onAttachAudio(url, duration);
|
||||||
|
}
|
||||||
|
|
||||||
handleSend(e) {
|
handleSend(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const message = this.state.message.trim();
|
const message = this.state.message.trim();
|
||||||
@@ -137,6 +146,13 @@ class SendMessage extends React.PureComponent {
|
|||||||
|
|
||||||
/* Send on Enter key */
|
/* Send on Enter key */
|
||||||
handleKeyPress(e) {
|
handleKeyPress(e) {
|
||||||
|
if (this.state.audioRec) {
|
||||||
|
// Ignore key presses while audio is being recorded.
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Remove this if you don't want Enter to trigger send
|
// Remove this if you don't want Enter to trigger send
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
// Have Shift-Enter insert a line break instead
|
// Have Shift-Enter insert a line break instead
|
||||||
@@ -192,7 +208,7 @@ class SendMessage extends React.PureComponent {
|
|||||||
<div id="send-message-panel">
|
<div id="send-message-panel">
|
||||||
{!this.props.disabled ?
|
{!this.props.disabled ?
|
||||||
<>
|
<>
|
||||||
{this.props.onAttachFile ?
|
{this.props.onAttachFile && !this.state.audioRec ?
|
||||||
<>
|
<>
|
||||||
<a href="#" onClick={(e) => {e.preventDefault(); this.attachImage.click();}} title="Add image">
|
<a href="#" onClick={(e) => {e.preventDefault(); this.attachImage.click();}} title="Add image">
|
||||||
<i className="material-icons secondary">photo</i>
|
<i className="material-icons secondary">photo</i>
|
||||||
@@ -205,14 +221,25 @@ class SendMessage extends React.PureComponent {
|
|||||||
null}
|
null}
|
||||||
{this.props.noInput ?
|
{this.props.noInput ?
|
||||||
(this.state.quote ? quote : <div className="hr thin" />) :
|
(this.state.quote ? quote : <div className="hr thin" />) :
|
||||||
|
(this.state.audioRec ?
|
||||||
|
<AudioRecorder
|
||||||
|
onDeleted={_ => this.setState({audioRec: false})}
|
||||||
|
onFinished={this.handleAttachAudio}/> :
|
||||||
<textarea id="sendMessage" placeholder={prompt}
|
<textarea id="sendMessage" placeholder={prompt}
|
||||||
value={this.state.message} onChange={this.handleMessageTyping}
|
value={this.state.message} onChange={this.handleMessageTyping}
|
||||||
onKeyPress={this.handleKeyPress}
|
onKeyPress={this.handleKeyPress}
|
||||||
ref={(ref) => {this.messageEditArea = ref;}}
|
ref={(ref) => {this.messageEditArea = ref;}}
|
||||||
autoFocus />}
|
autoFocus />)}
|
||||||
|
{this.state.message || !this.state.audioEnabled ?
|
||||||
<a href="#" onClick={this.handleSend} title="Send">
|
<a href="#" onClick={this.handleSend} title="Send">
|
||||||
<i className="material-icons">send</i>
|
<i className="material-icons">send</i>
|
||||||
</a>
|
</a> :
|
||||||
|
!this.state.audioRec ?
|
||||||
|
<a href="#" onClick={e => {e.preventDefault(); this.setState({audioRec: true})}} title="Voice">
|
||||||
|
<i className="material-icons">mic</i>
|
||||||
|
</a> :
|
||||||
|
null
|
||||||
|
}
|
||||||
<input type="file" ref={(ref) => {this.attachFile = ref;}}
|
<input type="file" ref={(ref) => {this.attachFile = ref;}}
|
||||||
onChange={this.handleAttachFile} style={{display: 'none'}} />
|
onChange={this.handleAttachFile} style={{display: 'none'}} />
|
||||||
<input type="file" ref={(ref) => {this.attachImage = ref;}} accept="image/*"
|
<input type="file" ref={(ref) => {this.attachImage = ref;}} accept="image/*"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import Tinode from 'tinode-sdk';
|
import { Tinode } from 'tinode-sdk';
|
||||||
|
|
||||||
import AvatarCrop from './avatar-crop.jsx';
|
import AvatarCrop from './avatar-crop.jsx';
|
||||||
import AvatarUpload from './avatar-upload.jsx';
|
import AvatarUpload from './avatar-upload.jsx';
|
||||||
|
|||||||
+6746
-259
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1284
-985
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
// This is a generated file. Don't edit.
|
// This is a generated file. Don't edit.
|
||||||
|
|
||||||
const PACKAGE_VERSION = "0.18.2-beta1";
|
const PACKAGE_VERSION = "0.19.0-alpha1";
|
||||||
|
|||||||
+2
-4
@@ -52,8 +52,8 @@ module.exports = (env, argv) => {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
performance: {
|
performance: {
|
||||||
maxEntrypointSize: 294912,
|
maxEntrypointSize: 320000,
|
||||||
maxAssetSize: 294912
|
maxAssetSize: 320000
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new CopyPlugin({
|
new CopyPlugin({
|
||||||
@@ -66,8 +66,6 @@ module.exports = (env, argv) => {
|
|||||||
'react': 'React',
|
'react': 'React',
|
||||||
'react-dom': 'ReactDOM',
|
'react-dom': 'ReactDOM',
|
||||||
'react-intl': 'ReactIntl',
|
'react-intl': 'ReactIntl',
|
||||||
'firebase/app': 'firebase',
|
|
||||||
'firebase/messaging': ['firebase', 'messaging'],
|
|
||||||
'tinode-sdk': 'Tinode',
|
'tinode-sdk': 'Tinode',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user