Compare commits

..
49 Commits
Author SHA1 Message Date
or-else cf5fd9753c release version 0.17.2 and rebuild 2021-06-21 14:47:53 -07:00
or-else a465fcf935 update dependencies, bump version and rebuild 2021-06-20 10:47:27 -07:00
or-else 2d01e0af85 fixed topic subscription on page reload 2021-06-18 19:13:59 -07:00
or-else 3fcd118af8 bumped version and rebuilt with fixes 2021-06-18 17:58:56 -07:00
or-else cdf27b4d48 fixed layout of tabs in new topic view 2021-06-18 17:58:32 -07:00
or-else ab993e36b9 bumped version, updated dependencies, rebuilt 2021-06-15 14:53:08 -07:00
or-else 9277db4dcd rebuild 2021-06-10 19:55:42 -07:00
or-else 9b76577bb9 fix occasional breakage in makeImageDataUrl 2021-06-10 19:54:42 -07:00
or-else c57fd6e539 fix DB initialization and teardown, wrong upload parameters order 2021-06-09 15:58:07 -07:00
or-else 2ffe42741d merge devel 2021-06-04 18:23:41 -07:00
or-else 5d97746d9d remove support for obscure browsers 2021-06-04 18:21:51 -07:00
or-else e22a84c8ad Merge branch 'master' of https://github.com/tinode/webapp 2021-06-03 18:20:58 -07:00
or-else 2b12baa166 updated dependencies, bumped version to 0.17.1 and rebuilt 2021-06-03 18:20:12 -07:00
or-else 2ecb522b44 updated dependencies including react-intl (previously broken) 2021-06-02 17:43:43 -07:00
Gene 6527ae8613 Update feature_request.md 2021-06-02 00:26:30 +03:00
Gene 10aa29e68b Update issue templates 2021-06-02 00:25:59 +03:00
or-else d944ddccd5 bump version to 0.17.1-rc and rebuild 2021-05-30 11:33:33 -07:00
or-else ef469ce419 rebuilt 2021-05-19 15:19:06 -07:00
or-else c1b3040f8a ad references to native apps to manifest 2021-05-19 15:13:08 -07:00
or-else bf0c818c28 pass topic name in form response 2021-05-19 15:13:05 -07:00
or-else 92f7464477 ignore trailing deleted ranges for message previews 2021-05-19 15:11:06 -07:00
or-else 034944877c fix cache issue on logout 2021-05-19 13:44:24 -07:00
or-else 694b93ebe8 merge devel 2021-05-10 17:00:00 -07:00
or-else 29e5be4e01 pass topic name in form response 2021-05-10 16:50:24 -07:00
or-else 02e23c885a tabs to spaces, typo in comment 2021-05-10 11:40:58 -07:00
or-else 74d5473a8b adding support for out of band avatars 2021-05-10 11:38:53 -07:00
or-else 02e1a28e89 rename vcard to theCard 2021-05-09 14:12:46 -07:00
or-else 2c61ab26c8 prepare for foreground notifications 2021-05-09 14:07:41 -07:00
or-else eeb14cf752 better cache handling 2021-05-09 14:07:05 -07:00
or-else e97e708c01 adding another generated file 2021-05-08 18:01:47 -07:00
or-else aa8d60cc1d a bit of cleanup, better naming of the app cache 2021-05-08 18:01:14 -07:00
or-else 62259ab331 releasing 0.17.0 2021-05-06 11:04:52 -07:00
or-else c010d1d17a bump version and rebuild 2021-05-03 16:39:46 -07:00
or-else 91e3f23d4e rebuilt with sdk fixes 2021-05-03 16:20:37 -07:00
or-else 1d6b970d7b removed "undefined" color 2021-05-02 16:48:53 -07:00
or-else 430b2db9aa fix bubble being too wide on mobile 2021-05-02 09:48:07 -07:00
or-else 4a480d71cc fix the issue with loading topics from cache 2021-05-01 20:07:24 -07:00
or-else 1537016942 fix incorrect initialization of promise when initializing pushes 2021-05-01 08:14:54 -07:00
or-else 1023e80fa6 another really monor visual tweak for large images 2021-04-30 11:45:34 -07:00
or-else 91e1959833 bump version to -rc2 and rebuild 2021-04-29 13:44:06 -07:00
or-else 0add66f607 a few more visual tweaks 2021-04-29 10:41:03 -07:00
or-else 273e34de2c clear reconnect timer on new connection 2021-04-28 17:05:00 -07:00
or-else 7adc06332e rebuilt 2021-04-28 16:33:24 -07:00
or-else 271fbc4b02 message timestamp is always shown on the bottom right now 2021-04-28 16:32:36 -07:00
or-else f6acbaab21 i18n of drafty preview elements 2021-04-28 13:34:14 -07:00
or-else 8715251e9b explain the workaround 2021-04-26 20:21:35 -07:00
or-else 8c5660173c workaround for what looks like a chrome bug 2021-04-26 19:41:58 -07:00
or-else 52ec124cd6 hopefully last fix 2021-04-25 16:53:40 -07:00
or-else cf423ff5eb formatting fixes 2021-04-25 16:52:30 -07:00
49 changed files with 4880 additions and 4568 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ assignees: ''
--- ---
If you ant to ask a question, please post to https://groups.google.com/d/forum/tinode instead. If you want to ask a question, please post to https://groups.google.com/d/forum/tinode instead.
--- ---
**Describe the bug** **Describe the bug**
+1 -1
View File
@@ -7,7 +7,7 @@ assignees: ''
--- ---
If you ant to ask a question, please post to https://groups.google.com/d/forum/tinode instead. If you want to ask a question, please post to https://groups.google.com/d/forum/tinode instead.
--- ---
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem? Please describe.**
+2 -2
View File
@@ -72,11 +72,11 @@ If you want to use the app with your own server and want web push notification t
### Desktop screenshot ### Desktop screenshot
<p align="center"> <p align="center">
<img src="web-desktop.jpeg" alt="Desktop web: full app" width=810 /> <img src="web-desktop.jpg" alt="Desktop web: full app" width=810 />
</p> </p>
### Mobile screenshots ### Mobile screenshots
<p align="center"> <p align="center">
<img src="web-mob-contacts.png" alt="Mobile web: contacts" width=250 /> <img src="web-mob-chat.png" alt="Mobile web: chat" width=250 /> <img src="web-mob-info-1.png" alt="Mobile web: topic info" width=250 /> <img src="web-mob-new-chat.png" alt="Mobile web: start new chat" width=250 /> <img src="web-mob-acc-settings.png" alt="Mobile web: account settings" width=250 /> <kbd><img src="web-mob-contacts.png" alt="Mobile web: contacts" width=250 /></kbd> <kbd><img src="web-mob-chat.png" alt="Mobile web: chat" width=250 /></kbd> <kbd><img src="web-mob-info.png" alt="Mobile web: topic info" width=250 /></kbd> <kbd><img src="web-mob-new-chat.png" alt="Mobile web: start new chat" width=250 /></kbd> <kbd><img src="web-mob-acc-settings.png" alt="Mobile web: account settings" width=250 /></kbd>
</p> </p>
+70 -48
View File
@@ -254,16 +254,16 @@ textarea {
/* Chrome & other webkit browsers */ /* Chrome & other webkit browsers */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 0.6rem; width: 0.6rem;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.2); -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.2);
background-color: rgba(255,255,255,0.1); background-color: rgba(255,255,255,0.1);
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.3); background-color: rgba(0,0,0,0.3);
} }
/* Firefox only */ /* Firefox only */
@@ -544,7 +544,7 @@ div.alert .content {
margin-left: 1rem; margin-left: 1rem;
} }
/* Extra spece on the right */ /* Extra space on the right */
.space-right { .space-right {
margin-right: 0.25rem; margin-right: 0.25rem;
} }
@@ -822,6 +822,7 @@ i.search {
margin-top: 0.25rem; margin-top: 0.25rem;
margin-bottom: auto; margin-bottom: auto;
height: 100%; height: 100%;
scroll-behavior: smooth;
} }
.contact-box { .contact-box {
@@ -1114,7 +1115,7 @@ i.search {
} }
.avatar-upload .inputfile:focus + label.round { .avatar-upload .inputfile:focus + label.round {
outline: none; outline: none;
} }
.avatar-upload .inputfile:focus + label, .avatar-upload .inputfile:focus + label,
@@ -1387,6 +1388,7 @@ i.search {
list-style: none; list-style: none;
padding: 0 0 3.5rem 0; padding: 0 0 3.5rem 0;
margin: 0 0.5rem; margin: 0 0.5rem;
scroll-behavior: smooth;
} }
/* Group chats have avatars on the left. Make left margin smaller */ /* Group chats have avatars on the left. Make left margin smaller */
@@ -1414,10 +1416,10 @@ i.search {
.chat-box .bubble { .chat-box .bubble {
position: relative; position: relative;
display: flex; display: flex;
max-width: 40rem; max-width: 40rem;
min-width: 9rem; min-width: 9rem;
height: auto; height: auto;
padding: 0.75rem 0.5rem 0.75rem 1.25rem; padding: 0.75rem 0.5rem 0.75rem 0.5rem;
margin: 0; margin: 0;
text-align: left; text-align: left;
border-radius: 0.5rem; border-radius: 0.5rem;
@@ -1446,10 +1448,12 @@ i.search {
.chat-box .left .bubble { .chat-box .left .bubble {
background-color: #c5e1a5; background-color: #c5e1a5;
margin-right: 0.5rem;
} }
.chat-box .right .bubble { .chat-box .right .bubble {
background-color: #fafafa; background-color: #fafafa;
margin-left: 1.75rem;
} }
.chat-box .center .bubble { .chat-box .center .bubble {
@@ -1504,12 +1508,21 @@ i.search {
margin-right: 0.8rem; margin-right: 0.8rem;
} }
/* Message text + timestamp and delivery marker */
.chat-box .bubble div.content-meta {
width: 100%;
}
.chat-box .bubble div.message-content { .chat-box .bubble div.message-content {
position: relative;
color: #212121; color: #212121;
margin: 0; margin: 0;
width: 100%;
max-width: 36rem; max-width: 36rem;
overflow-wrap: break-word; overflow-wrap: break-word;
padding: 0 0.75rem 0 0.75rem;
} }
/* The timestamp is a floating element. Need to ensure div.message-content is expanded /* The timestamp is a floating element. Need to ensure div.message-content is expanded
* to accomodate it. It's usually called clearfix. */ * to accomodate it. It's usually called clearfix. */
.chat-box .bubble div.message-content:after { .chat-box .bubble div.message-content:after {
@@ -1520,28 +1533,21 @@ i.search {
visibility: hidden; visibility: hidden;
} }
.chat-box .timestamp-padding {
display: inline-block;
width: 4rem;
height: 0;
}
.chat-box .timestamp { .chat-box .timestamp {
position: relative; position: relative;
font-size: 80%; font-size: 80%;
color: #777; color: #777;
padding-left: 1rem; top: 0.35rem;
top: 0.5rem;
float: right; float: right;
white-space: nowrap; white-space: nowrap;
} }
/* Typing notification bubble */
.chat-box .typing {
display: inline-block;
border-radius: 0.5rem;
width: 2rem;
padding: 0.25rem 0.35rem;
vertical-align: middle;
text-align: center;
color: #aaa;
margin-top: 0.75rem;
}
/* The name of the person who sent the message /* The name of the person who sent the message
* (group chat only) * (group chat only)
*/ */
@@ -1649,42 +1655,39 @@ img.image-preview::before {
/* BEGIN Tabs */ /* BEGIN Tabs */
ul.tabbar { ul.tabbar {
flex-shrink: 0; display: flex;
flex-grow: 0; flex-grow: 0;
background: #eeeeee; flex-shrink: 0;
background: #eee;
list-style:none; list-style:none;
margin:0; margin: 0;
width:100%; width: 100%;
overflow:hidden; overflow: hidden;
padding:0; padding: 0;
padding-left:1rem; padding-left: 1rem;
} }
ul.tabbar > li { ul.tabbar > li {
float: left;
}
ul.tabbar > li > a {
position: relative; position: relative;
padding-top: 0.75rem; height: 2.4rem;
padding-left: 1rem;
padding-right: 1rem;
text-align: center;
line-height: 2rem;
font-weight: 400;
font-size: 1.3rem;
font-variant: small-caps;
text-decoration: none;
color: rgba(33,150,243,0.6);
overflow: hidden;
box-shadow: inset 0 -2px 2.5px -1px rgba(0,0,0,0.25); box-shadow: inset 0 -2px 2.5px -1px rgba(0,0,0,0.25);
} }
ul.tabbar li.active > a { ul.tabbar li.active {
color: #2196f3; background: #fff;
} }
ul.tabbar li.active > a:after { ul.tabbar li:after {
height: 0.10rem;
width: 100%;
content: "";
bottom: 0;
left: 0;
position: absolute;
background: #ccc;
}
ul.tabbar li.active:after {
height: 0.15rem; height: 0.15rem;
content: ""; content: "";
bottom: 0; bottom: 0;
@@ -1704,6 +1707,25 @@ ul.tabbar li.active > a:after {
width: 100%; width: 100%;
} }
} }
ul.tabbar > li > a {
position: relative;
padding-left: 1rem;
padding-right: 1rem;
text-align: center;
line-height: 2.4rem;
font-weight: 400;
font-size: 1.3rem;
font-variant: small-caps;
text-decoration: none;
color: rgba(33,150,243,0.6);
overflow: hidden;
}
ul.tabbar li.active > a {
color: #2196f3;
}
/* END Tabs */ /* END Tabs */
/* Miscellaneous */ /* Miscellaneous */
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -32,7 +32,7 @@
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@17/umd/react.development.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@17/umd/react.development.js"></script>
<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.umd.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5/react-intl.iife.js"></script>
<!-- Firebase for push notifications --> <!-- 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-app.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@8/firebase-messaging.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@8/firebase-messaging.js"></script>
+3 -3
View File
@@ -24,9 +24,9 @@
<link rel="stylesheet" href="css/base.min.css" /> <link rel="stylesheet" href="css/base.min.css" />
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@17.0.1/umd/react.production.min.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@17.0.1/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@17.0.1/umd/react-dom.production.min.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@17.0.1/umd/react-dom.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5.12.5/react-intl.umd.min.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5.18.3/react-intl.iife.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@8.2.9/firebase-app.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@8.6.7/firebase-app.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@8.2.9/firebase-messaging.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@8.6.7/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>
+17 -3
View File
@@ -1,6 +1,8 @@
{ {
"short_name": "Tinode chat", "name": "Tinode",
"name": "Tinode Web chat", "short_name": "Tinode",
"description": "Tinode Web App.\nSee more at https://tinode.co/",
"categories": ["chat", "communication", "productivity"],
"icons": [ "icons": [
{ {
"src":"img/logo96.png", "src":"img/logo96.png",
@@ -22,5 +24,17 @@
"background_color": "silver", "background_color": "silver",
"theme_color": "#3949AB", "theme_color": "#3949AB",
"display": "standalone", "display": "standalone",
"gcm_sender_id": "103953800507" "gcm_sender_id": "103953800507",
"prefer_related_applications": true,
"related_applications": [
{
"platform": "play",
"url": "https://play.google.com/store/apps/details?id=co.tinode.tindroidx",
"id": "co.tinode.tindroidx"
},
{
"platform": "itunes",
"url": "https://apps.apple.com/us/app/tinode/id1483763538"
}
]
} }
+2712 -2817
View File
File diff suppressed because it is too large Load Diff
+19 -24
View File
@@ -1,7 +1,7 @@
{ {
"name": "tinode-webapp", "name": "tinode-webapp",
"description": "Tinode messenger for the web", "description": "Tinode messenger for the web",
"version": "0.17.0-rc1", "version": "0.17.2",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/tinode/example-react-js.git" "url": "https://github.com/tinode/example-react-js.git"
@@ -16,14 +16,8 @@
"extract": "formatjs extract src/**/*.jsx --out-file src/i18n/ex/base-en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'", "extract": "formatjs extract src/**/*.jsx --out-file src/i18n/ex/base-en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'",
"compile": "formatjs compile" "compile": "formatjs compile"
}, },
"browserslist": "> 0.5%, not IE 11", "browserslist": "> 0.5%, not IE 11, not op_mini all, not and_uc >0",
"files": [ "files": [
"src/index.js",
"src/config.js",
"src/views/*.jsx",
"src/widgets/*.jsx",
"src/lib/*.js",
"src/i18n/*.json",
"umd/index.prod.js", "umd/index.prod.js",
"umd/index.prod.js.map", "umd/index.prod.js.map",
"umd/index.dev.js", "umd/index.dev.js",
@@ -40,7 +34,8 @@
"index.html", "index.html",
"manifest.json", "manifest.json",
"service-worker.js", "service-worker.js",
"firebase-init.js" "firebase-init.js",
"version.js"
], ],
"keywords": [ "keywords": [
"chat", "chat",
@@ -68,27 +63,27 @@
"author": "Tinode Authors <info@tinode.co>", "author": "Tinode Authors <info@tinode.co>",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"firebase": "^8.2.9", "firebase": "^8.6.7",
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-intl": "^5.12.5", "react-intl": "^5.20.2",
"tinode-sdk": "^0.17.0-rc1" "tinode-sdk": "^0.17.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.13.8", "@babel/core": "^7.14.5",
"@babel/preset-env": "^7.13.8", "@babel/preset-env": "^7.14.5",
"@babel/preset-react": "^7.12.13", "@babel/preset-react": "^7.14.5",
"@formatjs/cli": "^4.2.6", "@formatjs/cli": "^4.2.20",
"babel-loader": "^8.2.2", "babel-loader": "^8.2.2",
"babel-plugin-formatjs": "^10.2.6", "babel-plugin-formatjs": "^10.2.19",
"browserslist": "^4.16.3", "browserslist": "^4.16.5",
"copy-webpack-plugin": "^8.1.0", "copy-webpack-plugin": "^9.0.0",
"cssnano": "^5.0.1", "cssnano": "^5.0.6",
"glob": "^7.1.6", "glob": "^7.1.7",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"postcss": "^8.2.6", "postcss": "^8.3.2",
"postcss-cli": "^8.3.1", "postcss-cli": "^8.3.1",
"webpack": "^5.24.2", "webpack": "^5.38.1",
"webpack-cli": "^4.5.0" "webpack-cli": "^4.7.2"
} }
} }
+16 -11
View File
@@ -1,13 +1,18 @@
var fs = require('fs'); const fs = require('fs');
var version = require('../package.json').version; const version = require('../package.json').version;
fs.writeFile('./src/version.js', function writeVersion(fileName, useExport) {
'// This is a generated file. Don\'t edit.\n\n'+ fs.writeFile(fileName,
'export const PACKAGE_VERSION = "'+version+'";\n', '// This is a generated file. Don\'t edit.\n\n'+
{ flag: 'w' }, `${useExport ? 'export ' : ''}const PACKAGE_VERSION = "${version}";\n`,
function(err) { { flag: 'w' },
if (err) { (err) => {
return console.log(err); if (err) {
return console.log(err);
}
} }
} );
); }
writeVersion('./src/version.js', true);
writeVersion('./version.js', false);
+15 -10
View File
@@ -1,7 +1,8 @@
// Must be located at the root. // Must be located at the root.
importScripts('https://cdn.jsdelivr.net/npm/firebase@8.2.9/firebase-app.js'); importScripts('https://cdn.jsdelivr.net/npm/firebase@8.6.7/firebase-app.js');
importScripts('https://cdn.jsdelivr.net/npm/firebase@8.2.9/firebase-messaging.js'); importScripts('https://cdn.jsdelivr.net/npm/firebase@8.6.7/firebase-messaging.js');
importScripts('firebase-init.js'); importScripts('firebase-init.js');
importScripts('version.js');
// Channel to notify the webapp. // Channel to notify the webapp.
const webAppChannel = new BroadcastChannel('tinode-sw'); const webAppChannel = new BroadcastChannel('tinode-sw');
@@ -55,7 +56,7 @@ fbMessaging.onBackgroundMessage((payload) => {
} }
// Notify webapp that a message was received. // Notify webapp that a message was received.
webAppChannel.postMessage(payload); webAppChannel.postMessage(payload.data);
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(pushType == 'msg' ? 'new_message' : 'new_chat');
@@ -120,30 +121,34 @@ self.addEventListener('notificationclick', event => {
// This is needed for 'Add to Home Screen'. // This is needed for 'Add to Home Screen'.
self.addEventListener('fetch', event => { self.addEventListener('fetch', event => {
if (event.request.method != 'GET') {
return;
}
event.respondWith((async () => { event.respondWith((async () => {
// Try to find response in cache. // Try to find the response in the cache.
const cachedResponse = await caches.match(event.request); const cache = await caches.open(PACKAGE_VERSION);
const cachedResponse = await cache.match(event.request);
if (cachedResponse) { if (cachedResponse) {
return cachedResponse; return cachedResponse;
} }
// Not found in cache. // Not found in cache.
const response = await fetch(event.request); const response = await fetch(event.request);
if (!response || response.status != 200 || response.type != 'basic' || event.request.method != 'GET') { if (!response || response.status != 200 || response.type != 'basic') {
return response; return response;
} }
if (event.request.url && (event.request.url.startsWith('http://') || event.request.url.startsWith('https://'))) { if (event.request.url && (event.request.url.startsWith('http://') || event.request.url.startsWith('https://'))) {
const cache = await caches.open(self.version);
await cache.put(event.request, response.clone()); await cache.put(event.request, response.clone());
} }
return response; return response;
})()); })());
}); });
// This code get the human language & app version from the webapp. // This code gets the human language from the webapp.
// Locale is used for localization of strings, version for naming the cache.
self.addEventListener('message', event => { self.addEventListener('message', event => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
// The locale is used for selecting strings in an appropriate language.
self.locale = data.locale || ''; self.locale = data.locale || '';
self.baseLocale = self.locale.toLowerCase().split(/[-_]/)[0]; self.baseLocale = self.locale.toLowerCase().split(/[-_]/)[0];
self.version = data.version || 'default';
}); });
+11 -10
View File
@@ -1,7 +1,7 @@
import { PACKAGE_VERSION } from './version.js'; import { PACKAGE_VERSION } from './version.js';
// Name of this application, used in the User-Agent. // Name of this application, used in the User-Agent.
export const APP_NAME = 'TinodeWeb/' + (PACKAGE_VERSION || '0.16'); export const APP_NAME = 'TinodeWeb/' + (PACKAGE_VERSION || '0.17');
// API key. Use https://github.com/tinode/chat/tree/master/keygen to generate your own // API key. Use https://github.com/tinode/chat/tree/master/keygen to generate your own
export const API_KEY = 'AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K'; export const API_KEY = 'AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K';
@@ -22,11 +22,11 @@ export const RECEIVED_DELAY = 500;
// Delay before sending a read notification, milliseconds. // Delay before sending a read notification, milliseconds.
export const READ_DELAY = 1000; export const READ_DELAY = 1000;
// The shortest allowed tag length. Matches the value on the server. // The default shortest allowed tag length. Matches the value on the server.
export const MIN_TAG_LENGTH = 2; export const MIN_TAG_LENGTH = 2;
// The greatest allowed tag length. Matches the value on the server. // The default greatest allowed tag length. Matches the value on the server.
export const MAX_TAG_LENGTH = 96; export const MAX_TAG_LENGTH = 96;
// The maximum number of tags allowed. Matches the value on the server. // The default maximum number of tags allowed. Matches the value on the server.
export const MAX_TAG_COUNT = 16; export const MAX_TAG_COUNT = 16;
// Access mode for P2P subscriptions initiated by the current user. // Access mode for P2P subscriptions initiated by the current user.
@@ -56,17 +56,18 @@ export const BROKEN_IMAGE_SIZE = 32;
export const MESSAGES_PAGE = 24; export const MESSAGES_PAGE = 24;
// Default maximum in-band (included directly into the message) attachment size which fits into // Default maximum in-band (included directly into the message) attachment size which fits into
// a message of 256K in size. The actual binary size of the attachment should be smaller due to // a message of 256K in size. Used when the server-provided value is unavailable. The actual
// base64 encoding expansion and some overhead, for instance 1024 bytes. // binary size of the attachment should be smaller due to base64 encoding expansion and some overhead,
// for instance 1024 bytes.
// Increase this limit to a greater value in production, if desired. Also increase // Increase this limit to a greater value in production, if desired. Also increase
// max_message_size in the server config. // max_message_size in the server config.
export const MAX_INBAND_ATTACHMENT_SIZE = 262144; export const MAX_INBAND_ATTACHMENT_SIZE = 262144;
// Absolute maximum attachment size to be used with the server = 8MB. Increase to // Default absolute maximum attachment size to be used with the server = 8MB.
// something like 100MB in production. // Used when the server-provided value is unavailable. Increase to something like 100MB in production.
export const MAX_EXTERN_ATTACHMENT_SIZE = 1 << 23; export const MAX_EXTERN_ATTACHMENT_SIZE = 1 << 23;
// Maximum allowed linear dimension of an inline image in pixels. Larer images will be shrunk // Maximum allowed linear dimension (pixels) of an image sent inline. Larger images will be shrunk
// to make the larger dimension fit under this size. You may want to adjust it to 1600 or // to make the larger dimension fit under this size. You may want to adjust it to 1600 or
// 2400 for production. // 2400 for production.
export const MAX_IMAGE_DIM = 1024; export const MAX_IMAGE_DIM = 1024;
@@ -85,7 +86,7 @@ export const MAX_TITLE_LENGTH = 60;
export const MESSAGE_PREVIEW_LENGTH = 80; export const MESSAGE_PREVIEW_LENGTH = 80;
// Link for "Contact Us". // Link for "Contact Us".
export const LINK_CONTACT_US = 'email:info@tinode.co'; export const LINK_CONTACT_US = 'email:support@tinode.co';
// Link to Privacy Policy. // Link to Privacy Policy.
export const LINK_PRIVACY_POLICY = 'https://tinode.co/privacy.html'; export const LINK_PRIVACY_POLICY = 'https://tinode.co/privacy.html';
+4 -1
View File
@@ -172,5 +172,8 @@
"tags_editor_no_tags": "Schlagworte hinzufügen", "tags_editor_no_tags": "Schlagworte hinzufügen",
"title_manage_tags": "Verwalten", "title_manage_tags": "Verwalten",
"channel_prompt": "Dies ist ein Sender", "channel_prompt": "Dies ist ein Sender",
"channel": "Sender" "channel": "Sender",
"drafty_attachment": "Dateianhang",
"drafty_form": "Formular:",
"drafty_image": "Bild"
} }
+3
View File
@@ -43,6 +43,9 @@
"delete_messages_warning": "Are you sure you want to delete all messages for everyone? It cannot be undone.", "delete_messages_warning": "Are you sure you want to delete all messages for everyone? It cannot be undone.",
"deleted_content": "content deleted", "deleted_content": "content deleted",
"download_action": "download", "download_action": "download",
"drafty_attachment": "Attachment",
"drafty_form": "Form:",
"drafty_image": "Picture",
"email_dative": "email", "email_dative": "email",
"email_prompt": "Email, e.g. jdoe@example.com", "email_prompt": "Email, e.g. jdoe@example.com",
"enable_peers_messaging": "Enable", "enable_peers_messaging": "Enable",
+4 -1
View File
@@ -172,5 +172,8 @@
"tags_editor_no_tags": "Añadir etiquetas", "tags_editor_no_tags": "Añadir etiquetas",
"title_manage_tags": "Gestionar", "title_manage_tags": "Gestionar",
"channel_prompt": "Este es un canal", "channel_prompt": "Este es un canal",
"channel": "canal" "channel": "canal",
"drafty_attachment": "Archivo",
"drafty_form": "Formulario:",
"drafty_image": "Imagen"
} }
+13 -1
View File
@@ -175,6 +175,18 @@
"defaultMessage": "download", "defaultMessage": "download",
"description": "Call to action [download]" "description": "Call to action [download]"
}, },
"drafty_attachment": {
"defaultMessage": "Attachment",
"description": "Comment for attachment in drafty preview"
},
"drafty_form": {
"defaultMessage": "Form:",
"description": "Comment for form in drafty preview"
},
"drafty_image": {
"defaultMessage": "Picture",
"description": "Comment for embedded images in drafty preview"
},
"email_dative": { "email_dative": {
"defaultMessage": "email", "defaultMessage": "email",
"description": "Dative case of 'email', i.e. 'email' in 'by email'" "description": "Dative case of 'email', i.e. 'email' in 'by email'"
@@ -693,4 +705,4 @@
"defaultMessage": "confirm", "defaultMessage": "confirm",
"description": "Validate credentail call to action" "description": "Validate credentail call to action"
} }
} }
+4 -1
View File
@@ -175,5 +175,8 @@
"messages_not_readable": "메시지에 액세스 할 수 없습니다", "messages_not_readable": "메시지에 액세스 할 수 없습니다",
"deleted_content": "내용이 삭제되었습니다", "deleted_content": "내용이 삭제되었습니다",
"channel_prompt": "이 채널입니다", "channel_prompt": "이 채널입니다",
"channel": "채널" "channel": "채널",
"drafty_attachment": "부착",
"drafty_form": "형태:",
"drafty_image": "이미지"
} }
+4 -1
View File
@@ -172,5 +172,8 @@
"cannot_initiate_file_upload": "Nu se poate face încărcarea.", "cannot_initiate_file_upload": "Nu se poate face încărcarea.",
"tags_not_found": "Niciun tag definit. Adaugă.", "tags_not_found": "Niciun tag definit. Adaugă.",
"tags_editor_no_tags": "Adaugă tag-uri", "tags_editor_no_tags": "Adaugă tag-uri",
"title_manage_tags": "Administrează" "title_manage_tags": "Administrează",
"drafty_attachment": "Atasament",
"drafty_form": "Formă:",
"drafty_image": "Imagine"
} }
+4 -1
View File
@@ -172,5 +172,8 @@
"no_connection": "Нет связи", "no_connection": "Нет связи",
"code_doesnot_match": "Код не совпадает", "code_doesnot_match": "Код не совпадает",
"channel_prompt": "Создать канал", "channel_prompt": "Создать канал",
"channel": "канал" "channel": "канал",
"drafty_attachment": "Аттачмент",
"drafty_form": "Форма:",
"drafty_image": "Картинка"
} }
+4 -1
View File
@@ -172,5 +172,8 @@
"no_connection": "无连接", "no_connection": "无连接",
"code_doesnot_match": "代码不匹配", "code_doesnot_match": "代码不匹配",
"channel_prompt": "这是一个频道", "channel_prompt": "这是一个频道",
"channel": "频道" "channel": "频道",
"drafty_attachment": "附件",
"drafty_form": "形式:",
"drafty_image": "图像"
} }
+14 -8
View File
@@ -6,10 +6,16 @@ import { bytesToHumanSize } from './strformat.js'
export const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml']; export const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml'];
export const MIME_EXTENSIONS = ['jpg', 'gif', 'png', 'svg', 'svg']; export const MIME_EXTENSIONS = ['jpg', 'gif', 'png', 'svg', 'svg'];
// Make a data URL from public.photo // Make a data: URL from public.photo
export function makeImageUrl(photo) { export function makeImageDataUrl(photo) {
return (photo && photo.type && photo.data) ? if (photo) {
'data:image/' + photo.type + ';base64,' + photo.data : null; if (photo.data && photo.type) {
const mime = photo.type.startsWith('image/') ? photo.type : ('image/' + photo.type);
return 'data:' + mime + ';base64,' + photo.data;
}
return photo.ref;
}
return null;
} }
// Calculate linear dimensions for scaling image down to fit under a certain size. // Calculate linear dimensions for scaling image down to fit under a certain size.
@@ -30,12 +36,12 @@ export function fitImageSize(width, height, maxWidth, maxHeight, forceSquare) {
maxWidth = maxHeight = Math.min(maxWidth, maxHeight); maxWidth = maxHeight = Math.min(maxWidth, maxHeight);
} }
let scale = Math.min( const scale = Math.min(
Math.min(width, maxWidth) / width, Math.min(width, maxWidth) / width,
Math.min(height, maxHeight) / height Math.min(height, maxHeight) / height
); );
let size = { const size = {
dstWidth: (width * scale) | 0, dstWidth: (width * scale) | 0,
dstHeight: (height * scale) | 0, dstHeight: (height * scale) | 0,
}; };
@@ -96,7 +102,7 @@ export function imageScaled(fileOrBlob, maxWidth, maxHeight, maxSize, forceSquar
ctx.drawImage(this, dim.xoffset, dim.yoffset, dim.srcWidth, dim.srcHeight, ctx.drawImage(this, dim.xoffset, dim.yoffset, dim.srcWidth, dim.srcHeight,
0, 0, dim.dstWidth, dim.dstHeight); 0, 0, dim.dstWidth, dim.dstHeight);
const mime = SUPPORTED_IMAGE_FORMATS.indexOf(fileOrBlob.type) < 0 ? 'image/jpeg' : fileOrBlob.type; const mime = SUPPORTED_IMAGE_FORMATS.includes(fileOrBlob.type) ? fileOrBlob.type : 'image/jpeg';
// Generate blob to check size of the image. // Generate blob to check size of the image.
let blob = await new Promise(resolve => canvas.toBlob(resolve, mime)); let blob = await new Promise(resolve => canvas.toBlob(resolve, mime));
if (!blob) { if (!blob) {
@@ -119,7 +125,7 @@ export function imageScaled(fileOrBlob, maxWidth, maxHeight, maxSize, forceSquar
} }
canvas = null; canvas = null;
onSuccess(blob, mime, dim.dstWidth, dim.dstHeight, fileNameForMime(fileOrBlob.name, mime)); onSuccess(mime, blob, dim.dstWidth, dim.dstHeight, fileNameForMime(fileOrBlob.name, mime));
}; };
img.src = URL.createObjectURL(fileOrBlob); img.src = URL.createObjectURL(fileOrBlob);
} }
+26 -14
View File
@@ -19,23 +19,35 @@ export function updateFavicon(count) {
document.title = (count > 0 ? '('+count+') ' : '') + 'Tinode'; document.title = (count > 0 ? '('+count+') ' : '') + 'Tinode';
} }
// Create VCard which represents topic 'public' info // Create theCard which represents user's or topic's "public" info.
export function vcard(fn, imageDataUrl) { export function theCard(fn, imageUrl, imageMimeType) {
let card = null; let card = null;
fn = fn && fn.trim();
if ((fn && fn.trim()) || imageDataUrl) { if (fn) {
card = {}; card = {
if (fn) { fn: fn
card.fn = fn.trim(); };
}
if (imageDataUrl) {
const dataStart = imageDataUrl.indexOf(',');
card.photo = dataStart >= 0 ? {
data: imageDataUrl.substring(dataStart+1),
type: 'jpg'
} : imageDataUrl;
}
} }
if (imageUrl) {
card = card || {};
let mimeType = imageMimeType;
// Is this a data URL "data:[<mediatype>][;base64],<data>"?
const matches = /^data:(image\/[-a-z0-9+.]+)?(;base64)?,/i.exec(imageUrl);
if (matches) {
mimeType = matches[1];
card.photo = {
data: imageUrl.substring(imageUrl.indexOf(',') + 1)
};
} else {
card.photo = {
ref: imageUrl
};
}
card.photo.type = (mimeType || 'image/jpeg').substring('image/'.length);
}
return card; return card;
} }
+27 -6
View File
@@ -173,7 +173,10 @@
"tags_editor_no_tags": "Schlagworte hinzufügen", "tags_editor_no_tags": "Schlagworte hinzufügen",
"title_manage_tags": "Verwalten", "title_manage_tags": "Verwalten",
"channel_prompt": "Dies ist ein Sender", "channel_prompt": "Dies ist ein Sender",
"channel": "Sender" "channel": "Sender",
"drafty_attachment": "Dateianhang",
"drafty_form": "Formular:",
"drafty_image": "Bild"
}, },
"en": { "en": {
"action_block_contact": "Block Contact", "action_block_contact": "Block Contact",
@@ -220,6 +223,9 @@
"delete_messages_warning": "Are you sure you want to delete all messages for everyone? It cannot be undone.", "delete_messages_warning": "Are you sure you want to delete all messages for everyone? It cannot be undone.",
"deleted_content": "content deleted", "deleted_content": "content deleted",
"download_action": "download", "download_action": "download",
"drafty_attachment": "Attachment",
"drafty_form": "Form:",
"drafty_image": "Picture",
"email_dative": "email", "email_dative": "email",
"email_prompt": "Email, e.g. jdoe@example.com", "email_prompt": "Email, e.g. jdoe@example.com",
"enable_peers_messaging": "Enable", "enable_peers_messaging": "Enable",
@@ -525,7 +531,10 @@
"tags_editor_no_tags": "Añadir etiquetas", "tags_editor_no_tags": "Añadir etiquetas",
"title_manage_tags": "Gestionar", "title_manage_tags": "Gestionar",
"channel_prompt": "Este es un canal", "channel_prompt": "Este es un canal",
"channel": "canal" "channel": "canal",
"drafty_attachment": "Archivo",
"drafty_form": "Formulario:",
"drafty_image": "Imagen"
}, },
"ko": { "ko": {
"validate_credential_action": "확인", "validate_credential_action": "확인",
@@ -701,7 +710,10 @@
"messages_not_readable": "메시지에 액세스 할 수 없습니다", "messages_not_readable": "메시지에 액세스 할 수 없습니다",
"deleted_content": "내용이 삭제되었습니다", "deleted_content": "내용이 삭제되었습니다",
"channel_prompt": "이 채널입니다", "channel_prompt": "이 채널입니다",
"channel": "채널" "channel": "채널",
"drafty_attachment": "부착",
"drafty_form": "형태:",
"drafty_image": "이미지"
}, },
"ro": { "ro": {
"validate_credential_action": "confirmă", "validate_credential_action": "confirmă",
@@ -877,7 +889,10 @@
"cannot_initiate_file_upload": "Nu se poate face încărcarea.", "cannot_initiate_file_upload": "Nu se poate face încărcarea.",
"tags_not_found": "Niciun tag definit. Adaugă.", "tags_not_found": "Niciun tag definit. Adaugă.",
"tags_editor_no_tags": "Adaugă tag-uri", "tags_editor_no_tags": "Adaugă tag-uri",
"title_manage_tags": "Administrează" "title_manage_tags": "Administrează",
"drafty_attachment": "Atasament",
"drafty_form": "Formă:",
"drafty_image": "Imagine"
}, },
"ru": { "ru": {
"contacts_not_found": "Чатов нет<br />¯∖_(ツ)_/¯", "contacts_not_found": "Чатов нет<br />¯∖_(ツ)_/¯",
@@ -1053,7 +1068,10 @@
"no_connection": "Нет связи", "no_connection": "Нет связи",
"code_doesnot_match": "Код не совпадает", "code_doesnot_match": "Код не совпадает",
"channel_prompt": "Создать канал", "channel_prompt": "Создать канал",
"channel": "канал" "channel": "канал",
"drafty_attachment": "Аттачмент",
"drafty_form": "Форма:",
"drafty_image": "Картинка"
}, },
"zh": { "zh": {
"archived_contacts": "已归档联系人 ({count})", "archived_contacts": "已归档联系人 ({count})",
@@ -1229,6 +1247,9 @@
"no_connection": "无连接", "no_connection": "无连接",
"code_doesnot_match": "代码不匹配", "code_doesnot_match": "代码不匹配",
"channel_prompt": "这是一个频道", "channel_prompt": "这是一个频道",
"channel": "频道" "channel": "频道",
"drafty_attachment": "附件",
"drafty_form": "形式:",
"drafty_image": "图像"
} }
} }
+1 -1
View File
@@ -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.17.0-rc1"; export const PACKAGE_VERSION = "0.17.2";
+5 -5
View File
@@ -9,8 +9,8 @@ import InPlaceEdit from '../widgets/in-place-edit.jsx';
import TagManager from '../widgets/tag-manager.jsx'; import TagManager from '../widgets/tag-manager.jsx';
import { MAX_TITLE_LENGTH } from '../config.js'; import { MAX_TITLE_LENGTH } from '../config.js';
import { makeImageUrl } from '../lib/blob-helpers.js'; import { makeImageDataUrl } from '../lib/blob-helpers.js';
import { arrayEqual, asEmail, asPhone, vcard } from '../lib/utils.js'; import { arrayEqual, asEmail, asPhone, theCard } from '../lib/utils.js';
export default class AccGeneralView extends React.Component { export default class AccGeneralView extends React.Component {
constructor(props) { constructor(props) {
@@ -19,7 +19,7 @@ export default class AccGeneralView extends React.Component {
const me = this.props.tinode.getMeTopic(); const me = this.props.tinode.getMeTopic();
this.state = { this.state = {
fullName: me.public ? me.public.fn : undefined, fullName: me.public ? me.public.fn : undefined,
avatar: makeImageUrl(me.public ? me.public.photo : null), avatar: makeImageDataUrl(me.public ? me.public.photo : null),
tags: me.tags(), tags: me.tags(),
credentials: me.getCredentials() || [], credentials: me.getCredentials() || [],
addCredActive: false, addCredActive: false,
@@ -62,13 +62,13 @@ export default class AccGeneralView extends React.Component {
fn = fn.trim().substring(0, MAX_TITLE_LENGTH); fn = fn.trim().substring(0, MAX_TITLE_LENGTH);
if (fn) { if (fn) {
this.setState({fullName: fn}); this.setState({fullName: fn});
this.props.onUpdateAccount(undefined, vcard(fn, null)); this.props.onUpdateAccount(undefined, theCard(fn, null));
} }
} }
handleImageChanged(img) { handleImageChanged(img) {
this.setState({avatar: img}); this.setState({avatar: img});
this.props.onUpdateAccount(undefined, vcard(null, img || Tinode.DEL_CHAR)); this.props.onUpdateAccount(undefined, theCard(null, img || Tinode.DEL_CHAR));
} }
handleCredChange(e) { handleCredChange(e) {
+2 -2
View File
@@ -7,7 +7,7 @@ import CheckBox from '../widgets/checkbox.jsx';
import VisiblePassword from '../widgets/visible-password.jsx'; import VisiblePassword from '../widgets/visible-password.jsx';
import LocalStorageUtil from '../lib/local-storage.js'; import LocalStorageUtil from '../lib/local-storage.js';
import { vcard } from '../lib/utils.js'; import { theCard } from '../lib/utils.js';
import { MAX_TITLE_LENGTH } from '../config.js'; import { MAX_TITLE_LENGTH } from '../config.js';
@@ -65,7 +65,7 @@ export default class CreateAccountView extends React.PureComponent {
this.props.onCreateAccount( this.props.onCreateAccount(
this.state.login.trim(), this.state.login.trim(),
this.state.password.trim(), this.state.password.trim(),
vcard(this.state.fn.trim().substring(0, MAX_TITLE_LENGTH), this.state.imageDataUrl), theCard(this.state.fn.trim().substring(0, MAX_TITLE_LENGTH), this.state.imageDataUrl),
{'meth': 'email', 'val': this.state.email}); {'meth': 'email', 'val': this.state.email});
} }
+2 -2
View File
@@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl';
import AvatarUpload from '../widgets/avatar-upload.jsx'; import AvatarUpload from '../widgets/avatar-upload.jsx';
import { makeImageUrl } from '../lib/blob-helpers.js'; import { makeImageDataUrl } from '../lib/blob-helpers.js';
export default class EditAccountView extends React.Component { export default class EditAccountView extends React.Component {
constructor(props) { constructor(props) {
@@ -13,7 +13,7 @@ export default class EditAccountView extends React.Component {
const me = this.props.tinode.getMeTopic(); const me = this.props.tinode.getMeTopic();
this.state = { this.state = {
fullName: me.public ? me.public.fn : undefined, fullName: me.public ? me.public.fn : undefined,
avatar: makeImageUrl(me.public ? me.public.photo : null) avatar: makeImageDataUrl(me.public ? me.public.photo : null)
}; };
} }
+5 -5
View File
@@ -17,8 +17,8 @@ import TagManager from '../widgets/tag-manager.jsx';
import { MAX_TITLE_LENGTH, NO_ACCESS_MODE } from '../config.js'; import { MAX_TITLE_LENGTH, NO_ACCESS_MODE } from '../config.js';
import { makeImageUrl } from '../lib/blob-helpers.js'; import { makeImageDataUrl } from '../lib/blob-helpers.js';
import { arrayEqual, vcard } from '../lib/utils.js'; import { arrayEqual, theCard } from '../lib/utils.js';
const messages = defineMessages({ const messages = defineMessages({
requested: { requested: {
@@ -227,7 +227,7 @@ class InfoView extends React.Component {
muted: acs && acs.isMuted(), muted: acs && acs.isMuted(),
fullName: topic.public ? topic.public.fn : undefined, fullName: topic.public ? topic.public.fn : undefined,
avatar: makeImageUrl(topic.public ? topic.public.photo : null), avatar: makeImageDataUrl(topic.public ? topic.public.photo : null),
private: topic.private ? topic.private.comment : null, private: topic.private ? topic.private.comment : null,
address: topic.name, address: topic.name,
groupTopic: topic.isGroupType(), groupTopic: topic.isGroupType(),
@@ -282,7 +282,7 @@ class InfoView extends React.Component {
fn = fn.trim().substring(0, MAX_TITLE_LENGTH); fn = fn.trim().substring(0, MAX_TITLE_LENGTH);
if (this.state.fullName !== fn) { if (this.state.fullName !== fn) {
this.setState({fullName: fn}); this.setState({fullName: fn});
this.props.onTopicDescUpdate(this.props.topic, vcard(fn, null), null); this.props.onTopicDescUpdate(this.props.topic, theCard(fn, null), null);
} }
} }
@@ -296,7 +296,7 @@ class InfoView extends React.Component {
handleImageChanged(img) { handleImageChanged(img) {
this.setState({avatar: img}); this.setState({avatar: img});
this.props.onTopicDescUpdate(this.props.topic, vcard(null, img || Tinode.DEL_CHAR), null); this.props.onTopicDescUpdate(this.props.topic, theCard(null, img || Tinode.DEL_CHAR), null);
} }
handleMuted(ignored, checked) { handleMuted(ignored, checked) {
+2 -2
View File
@@ -15,7 +15,7 @@ export default class LoginView extends React.Component {
login: props.login, login: props.login,
password: '', password: '',
hostName: props.serverAddress, hostName: props.serverAddress,
saveToken: LocalStorageUtil.getObject('keep-logged-in') saveToken: props.persist
}; };
this.handleLoginChange = this.handleLoginChange.bind(this); this.handleLoginChange = this.handleLoginChange.bind(this);
this.handlePasswordChange = this.handlePasswordChange.bind(this); this.handlePasswordChange = this.handlePasswordChange.bind(this);
@@ -32,7 +32,7 @@ export default class LoginView extends React.Component {
} }
handleToggleSaveToken() { handleToggleSaveToken() {
LocalStorageUtil.setObject('keep-logged-in', !this.state.saveToken); this.props.onPersistenceChange(!this.state.saveToken);
this.setState({saveToken: !this.state.saveToken}); this.setState({saveToken: !this.state.saveToken});
} }
+8 -7
View File
@@ -18,7 +18,7 @@ import SendMessage from '../widgets/send-message.jsx';
import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY, MESSAGES_PAGE, import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY, MESSAGES_PAGE,
MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE, READ_DELAY } from '../config.js'; MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE, READ_DELAY } from '../config.js';
import { SUPPORTED_IMAGE_FORMATS, blobToBase64, filePasted, fileToBase64, import { SUPPORTED_IMAGE_FORMATS, blobToBase64, filePasted, fileToBase64,
imageScaled, makeImageUrl } from '../lib/blob-helpers.js'; imageScaled, makeImageDataUrl } from '../lib/blob-helpers.js';
import HashNavigation from '../lib/navigation.js'; import HashNavigation from '../lib/navigation.js';
import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js'; import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js';
@@ -261,7 +261,7 @@ class MessagesView extends React.Component {
if (topic.public) { if (topic.public) {
Object.assign(nextState, { Object.assign(nextState, {
title: topic.public.fn, title: topic.public.fn,
avatar: makeImageUrl(topic.public.photo) avatar: makeImageDataUrl(topic.public.photo)
}); });
} else { } else {
Object.assign(nextState, { Object.assign(nextState, {
@@ -337,10 +337,10 @@ class MessagesView extends React.Component {
} }
leave(oldTopicName) { leave(oldTopicName) {
if (!oldTopicName) { if (!oldTopicName || !this.props.tinode.isTopicCached(oldTopicName)) {
return; return;
} }
let oldTopic = this.props.tinode.getTopic(oldTopicName); const oldTopic = this.props.tinode.getTopic(oldTopicName);
if (oldTopic && oldTopic.isSubscribed()) { if (oldTopic && oldTopic.isSubscribed()) {
oldTopic.leave(false) oldTopic.leave(false)
.catch(() => { /* do nothing here */ }) .catch(() => { /* do nothing here */ })
@@ -391,7 +391,7 @@ class MessagesView extends React.Component {
if (desc.public) { if (desc.public) {
this.setState({ this.setState({
title: desc.public.fn, title: desc.public.fn,
avatar: makeImageUrl(desc.public.photo) avatar: makeImageDataUrl(desc.public.photo)
}); });
} else { } else {
this.setState({ this.setState({
@@ -578,12 +578,13 @@ class MessagesView extends React.Component {
params.set(key, data.resp[key]); params.set(key, data.resp[key]);
} }
} }
['name', 'seq'].map(function(key) { ['name', 'seq'].map((key) => {
if (data[key]) { if (data[key]) {
params.set(key, data[key]); params.set(key, data[key]);
} }
}); });
params.set('uid', this.props.myUserId); params.set('uid', this.props.myUserId);
params.set('topic', this.state.topic);
url.search = params; url.search = params;
window.open(url, '_blank'); window.open(url, '_blank');
} else { } else {
@@ -856,7 +857,7 @@ class MessagesView extends React.Component {
const user = topic.userDesc(thisFrom); const user = topic.userDesc(thisFrom);
if (user && user.public) { if (user && user.public) {
userName = user.public.fn; userName = user.public.fn;
userAvatar = makeImageUrl(user.public.photo); userAvatar = makeImageDataUrl(user.public.photo);
} }
userFrom = thisFrom; userFrom = thisFrom;
chatBoxClass='chat-box group'; chatBoxClass='chat-box group';
+2 -2
View File
@@ -10,7 +10,7 @@ import NewTopicGroup from '../widgets/new-topic-group.jsx';
import SearchContacts from '../widgets/search-contacts.jsx'; import SearchContacts from '../widgets/search-contacts.jsx';
import HashNavigation from '../lib/navigation.js'; import HashNavigation from '../lib/navigation.js';
import { vcard } from '../lib/utils.js'; import { theCard } from '../lib/utils.js';
const messages = defineMessages({ const messages = defineMessages({
search_for_contacts: { search_for_contacts: {
@@ -65,7 +65,7 @@ class NewTopicView extends React.Component {
handleNewGroupSubmit(name, dataUrl, priv, tags, isChannel) { handleNewGroupSubmit(name, dataUrl, priv, tags, isChannel) {
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab')); HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
this.props.onCreateTopic(undefined, vcard(name, dataUrl), priv, tags, isChannel); this.props.onCreateTopic(undefined, theCard(name, dataUrl), priv, tags, isChannel);
} }
handleGroupByID(topicName) { handleGroupByID(topicName) {
+6 -7
View File
@@ -36,16 +36,15 @@ export default class SettingsView extends React.PureComponent {
render() { render() {
const names = {def: "default", ws: "websocket", lp: "long polling"}; const names = {def: "default", ws: "websocket", lp: "long polling"};
var transportOptions = []; const transportOptions = [];
var instance = this; ['def', 'ws', 'lp'].map((item) => {
['def', 'ws', 'lp'].map(function(item) { const id = 'transport-' + item;
var id = 'transport-' + item; const name = names[item];
var name = names[item];
transportOptions.push( transportOptions.push(
<li key={item}> <li key={item}>
<input type="radio" id={id} name="transport-select" value={item} <input type="radio" id={id} name="transport-select" value={item}
checked={instance.state.transport === item} checked={this.state.transport === item}
onChange={instance.handleTransportSelected} /> onChange={this.handleTransportSelected} />
<label htmlFor={id}>{name}</label> <label htmlFor={id}>{name}</label>
</li> </li>
); );
+3 -6
View File
@@ -91,14 +91,9 @@ class SidepanelView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.handleLoginRequested = this.handleLoginRequested.bind(this);
this.handleNewTopic = this.handleNewTopic.bind(this); this.handleNewTopic = this.handleNewTopic.bind(this);
} }
handleLoginRequested(login, password) {
this.props.onLoginRequest(login, password);
}
handleNewTopic() { handleNewTopic() {
this.props.onBasicNavigate('newtpk'); this.props.onBasicNavigate('newtpk');
} }
@@ -146,7 +141,9 @@ class SidepanelView extends React.Component {
<LoginView <LoginView
login={this.props.login} login={this.props.login}
disabled={this.props.loginDisabled} disabled={this.props.loginDisabled}
onLogin={this.handleLoginRequested} /> : persist={this.props.persist}
onLogin={this.props.onLoginRequest}
onPersistenceChange={this.props.onPersistenceChange} /> :
view === 'register' ? view === 'register' ?
<CreateAccountView <CreateAccountView
+62 -29
View File
@@ -18,7 +18,7 @@ import SidepanelView from './sidepanel-view.jsx';
import { API_KEY, APP_NAME, DEFAULT_P2P_ACCESS_MODE, LOGGING_ENABLED, import { API_KEY, APP_NAME, DEFAULT_P2P_ACCESS_MODE, LOGGING_ENABLED,
MEDIA_BREAKPOINT, RECEIVED_DELAY } from '../config.js'; MEDIA_BREAKPOINT, RECEIVED_DELAY } from '../config.js';
import { PACKAGE_VERSION } from '../version.js'; import { PACKAGE_VERSION } from '../version.js';
import { base64ReEncode, makeImageUrl } from '../lib/blob-helpers.js'; import { base64ReEncode, makeImageDataUrl } from '../lib/blob-helpers.js';
import { detectServerAddress, isLocalHost, isSecureConnection } from '../lib/host-name.js'; import { detectServerAddress, isLocalHost, isSecureConnection } from '../lib/host-name.js';
import LocalStorageUtil from '../lib/local-storage.js'; import LocalStorageUtil from '../lib/local-storage.js';
import HashNavigation from '../lib/navigation.js'; import HashNavigation from '../lib/navigation.js';
@@ -79,6 +79,7 @@ class TinodeWeb extends React.Component {
this.handleVisibilityEvent = this.handleVisibilityEvent.bind(this); this.handleVisibilityEvent = this.handleVisibilityEvent.bind(this);
this.handleError = this.handleError.bind(this); this.handleError = this.handleError.bind(this);
this.handleLoginRequest = this.handleLoginRequest.bind(this); this.handleLoginRequest = this.handleLoginRequest.bind(this);
this.handlePersistenceChange = this.handlePersistenceChange.bind(this);
this.handleConnected = this.handleConnected.bind(this); this.handleConnected = this.handleConnected.bind(this);
this.handleAutoreconnectIteration = this.handleAutoreconnectIteration.bind(this); this.handleAutoreconnectIteration = this.handleAutoreconnectIteration.bind(this);
this.doLogin = this.doLogin.bind(this); this.doLogin = this.doLogin.bind(this);
@@ -175,6 +176,7 @@ class TinodeWeb extends React.Component {
login: '', login: '',
password: '', password: '',
persist: persist,
myUserId: null, myUserId: null,
liveConnection: navigator.onLine, liveConnection: navigator.onLine,
topicSelected: '', topicSelected: '',
@@ -226,11 +228,9 @@ class TinodeWeb extends React.Component {
viewportHeight: document.documentElement.clientHeight viewportHeight: document.documentElement.clientHeight
}); });
const keepLoggedIn = LocalStorageUtil.getObject('keep-logged-in');
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress, this.state.transport, this.tinode = TinodeWeb.tnSetup(this.state.serverAddress, this.state.transport,
this.props.intl.locale, keepLoggedIn, resolve); this.props.intl.locale, this.state.persist, resolve);
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;
@@ -251,9 +251,13 @@ class TinodeWeb extends React.Component {
}); });
} }
const token = keepLoggedIn ? LocalStorageUtil.getObject('auth-token') : undefined; // Parse and save the hash navigation params.
const parsedNav = HashNavigation.parseUrlHash(window.location.hash); const parsedNav = HashNavigation.parseUrlHash(window.location.hash);
// Read contacts from cache.
this.resetContactList();
const token = this.state.persist ? LocalStorageUtil.getObject('auth-token') : undefined;
if (token) { if (token) {
this.setState({autoLogin: true}); this.setState({autoLogin: true});
@@ -264,16 +268,12 @@ class TinodeWeb extends React.Component {
// Socket error // Socket error
this.handleError(err.message, 'err'); this.handleError(err.message, 'err');
}); });
delete parsedNav.params.info;
delete parsedNav.params.tab;
parsedNav.path[0] = '';
HashNavigation.navigateTo(HashNavigation.composeUrlHash(parsedNav.path, parsedNav.params));
} else if (!parsedNav.params.token) {
// No token, save possible topic name and navigating to blank state.
this.setState({requestedTopic: parsedNav.path[1]});
HashNavigation.navigateTo('');
} }
// Save possible topic name and navigate to blank state.
this.setState({requestedTopic: parsedNav.path[1]});
HashNavigation.navigateTo('');
this.readTimer = null; this.readTimer = null;
this.readTimerCallback = null; this.readTimerCallback = null;
@@ -297,10 +297,10 @@ class TinodeWeb extends React.Component {
} }
// Notifiy Tinode that a push message was received from the server. // Notifiy Tinode that a push message was received from the server.
handlePushMessage(payload) { handlePushMessage(data) {
if (payload.data.what == 'msg' && Tinode.isChannelTopicName(payload.data.topic)) { if (data.what == 'msg' && 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(payload.data.topic, payload.data.seq, 'fake-uid'); this.tinode.oobNotification(data.topic, data.seq, 'fake-uid');
} }
} }
@@ -320,24 +320,26 @@ class TinodeWeb extends React.Component {
this.checkForAppUpdate(reg); this.checkForAppUpdate(reg);
this.fbPush.useServiceWorker(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}));
// Google could not be bothered to mention that
// onTokenRefresh is never called. // Google could not be bothered to mention that onTokenRefresh is never called.
this.fbPush.onTokenRefresh(() => { this.fbPush.onTokenRefresh(() => {
this.requestPushToken(); this.requestPushToken();
}); });
// FCM pushes for channels have to be handled even when the app is in the foreground. // Handhe FCM pushes
this.fbPush.onMessage(this.handlePushMessage); // (a) for channels always,
// (b) pushes when the app is in foreground but has not focus.
this.fbPush.onMessage(payload => { this.handlePushMessage(payload.data); });
return reg; return reg;
}).catch((err) => { }).catch((err) => {
// registration failed :( // registration failed :(
onError("Failed to register service worker:", err); onError("Failed to register service worker:", err);
return new Promise.reject(err); return Promise.reject(err);
}); });
} catch (err) { } catch (err) {
onError("Failed to initialize push notifications", err); onError("Failed to initialize push notifications", err);
return new Promise.reject(err); return Promise.reject(err);
} }
} }
@@ -385,10 +387,16 @@ class TinodeWeb extends React.Component {
} }
// Topic for MessagesView selector. // Topic for MessagesView selector.
if (hash.path.length > 1 && hash.path[1] != this.state.topicSelected) { const topicName = hash.path[1];
this.setState({ if (Tinode.topicType(topicName) && topicName != this.state.topicSelected) {
topicSelected: Tinode.topicType(hash.path[1]) ? hash.path[1] : null const newState = {
}); topicSelected: topicName
};
const acs = this.tinode.getTopicAccessMode(topicName);
if (acs) {
newState.topicSelectedAcs = acs;
}
this.setState(newState);
} }
} else { } else {
// Empty hashpath // Empty hashpath
@@ -451,8 +459,29 @@ class TinodeWeb extends React.Component {
} }
} }
// Enable or disable saving the password and IndexedDB.
handlePersistenceChange(persist) {
if (persist) {
this.tinode.initStorage().then(() => {
console.log("storage initialized");
LocalStorageUtil.setObject('keep-logged-in', true);
this.setState({persist: true});
});
} else {
this.tinode.clearStorage().then(() => {
console.log("storage cleared");
LocalStorageUtil.setObject('keep-logged-in', false);
this.setState({persist: false});
});
}
}
// Connection succeeded. // Connection succeeded.
handleConnected() { handleConnected() {
// Just to be sure.
clearInterval(this.reconnectCountdown);
this.handleError();
const params = this.tinode.getServerInfo(); const params = this.tinode.getServerInfo();
this.setState({ this.setState({
serverVersion: params.ver + ' ' + (params.build ? params.build : 'none') serverVersion: params.ver + ' ' + (params.build ? params.build : 'none')
@@ -623,7 +652,7 @@ class TinodeWeb extends React.Component {
if (desc.public) { if (desc.public) {
this.setState({ this.setState({
sidePanelTitle: desc.public.fn, sidePanelTitle: desc.public.fn,
sidePanelAvatar: makeImageUrl(desc.public.photo) sidePanelAvatar: makeImageDataUrl(desc.public.photo)
}); });
} }
if (desc.acs) { if (desc.acs) {
@@ -998,6 +1027,7 @@ class TinodeWeb extends React.Component {
LocalStorageUtil.getObject('keep-logged-in')); 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.setState({ this.setState({
serverAddress: serverAddress, serverAddress: serverAddress,
@@ -1027,7 +1057,7 @@ class TinodeWeb extends React.Component {
if (enabled) { if (enabled) {
if (!this.state.firebaseToken) { if (!this.state.firebaseToken) {
const fcm = this.fbPush ? const fcm = this.fbPush ?
new Promise().resolve() : Promise.resolve() :
this.initFCMessaging(); this.initFCMessaging();
fcm.then(() => { fcm.then(() => {
return this.fbPush.requestPermission(); return this.fbPush.requestPermission();
@@ -1234,6 +1264,7 @@ class TinodeWeb extends React.Component {
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;
HashNavigation.navigateTo(''); HashNavigation.navigateTo('');
} }
@@ -1499,6 +1530,7 @@ class TinodeWeb extends React.Component {
title={this.state.sidePanelTitle} title={this.state.sidePanelTitle}
avatar={this.state.sidePanelAvatar} avatar={this.state.sidePanelAvatar}
login={this.state.login} login={this.state.login}
persist={this.state.persist}
myUserId={this.state.myUserId} myUserId={this.state.myUserId}
loginDisabled={this.state.loginDisabled} loginDisabled={this.state.loginDisabled}
loadSpinnerVisible={this.state.loadSpinnerVisible} loadSpinnerVisible={this.state.loadSpinnerVisible}
@@ -1526,6 +1558,7 @@ class TinodeWeb extends React.Component {
onSettings={this.handleSettings} onSettings={this.handleSettings}
onBasicNavigate={this.basicNavigator} onBasicNavigate={this.basicNavigator}
onLoginRequest={this.handleLoginRequest} onLoginRequest={this.handleLoginRequest}
onPersistenceChange={this.handlePersistenceChange}
onCreateAccount={this.handleNewAccountRequest} onCreateAccount={this.handleNewAccountRequest}
onUpdateAccount={this.handleUpdateAccountRequest} onUpdateAccount={this.handleUpdateAccountRequest}
onUpdateAccountTags={this.handleUpdateAccountTagsRequest} onUpdateAccountTags={this.handleUpdateAccountTagsRequest}
+2 -2
View File
@@ -3,7 +3,7 @@ import React from 'react';
import LetterTile from './letter-tile.jsx'; import LetterTile from './letter-tile.jsx';
import { AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE } from '../config.js'; import { AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE } from '../config.js';
import { imageScaled, blobToBase64, makeImageUrl } from '../lib/blob-helpers.js'; import { imageScaled, blobToBase64, makeImageDataUrl } from '../lib/blob-helpers.js';
export default class AvatarUpload extends React.Component { export default class AvatarUpload extends React.Component {
constructor(props) { constructor(props) {
@@ -28,7 +28,7 @@ export default class AvatarUpload extends React.Component {
(mime, blob) => { (mime, blob) => {
// Convert blob to base64-encoded bits. // Convert blob to base64-encoded bits.
blobToBase64(blob, (unused, base64bits) => { blobToBase64(blob, (unused, base64bits) => {
const du = makeImageUrl({data: base64bits, type: mime}); const du = makeImageDataUrl({data: base64bits, type: mime});
this.setState({dataUrl: du}); this.setState({dataUrl: du});
this.props.onImageChanged(du); this.props.onImageChanged(du);
}); });
+14 -5
View File
@@ -128,9 +128,11 @@ export default class ChatMessage extends React.Component {
null} null}
<div> <div>
<div className={bubbleClass}> <div className={bubbleClass}>
<div className="message-content"> <div className="content-meta">
{content} <div className="message-content">
{attachments} {content}
{attachments}
</div>
{this.props.timestamp ? {this.props.timestamp ?
<ReceivedMarker <ReceivedMarker
timestamp={this.props.timestamp} timestamp={this.props.timestamp}
@@ -187,15 +189,22 @@ function draftyFormatter(style, data, values, key) {
if (data) { if (data) {
attr.className = 'inline-image'; attr.className = 'inline-image';
const dim = fitImageSize(data.width, data.height, const dim = fitImageSize(data.width, data.height,
Math.min(this.props.viewportWidth - REM_SIZE * 4, REM_SIZE * 36), REM_SIZE * 24, false) || Math.min(this.props.viewportWidth - REM_SIZE * 6.5, REM_SIZE * 34.5), REM_SIZE * 24, false) ||
{dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE}; {dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
attr.style = { width: dim.dstWidth + 'px', height: dim.dstHeight + 'px' }; attr.style = {
width: dim.dstWidth + 'px',
height: dim.dstHeight + 'px',
// Looks like a Chrome bug: broken image does not respect 'width' and 'height'.
minWidth: dim.dstWidth + 'px',
minHeight: dim.dstHeight + 'px'
};
if (!Drafty.isProcessing(data)) { if (!Drafty.isProcessing(data)) {
attr.src = this.props.tinode.authorizeURL(sanitizeImageUrl(attr.src)); attr.src = this.props.tinode.authorizeURL(sanitizeImageUrl(attr.src));
attr.alt = data.name; attr.alt = data.name;
if (attr.src) { if (attr.src) {
attr.onClick = this.handleImagePreview; attr.onClick = this.handleImagePreview;
attr.className += ' image-clickable'; attr.className += ' image-clickable';
attr.loading = 'lazy';
} else { } else {
attr.src = 'img/broken_image.png'; attr.src = 'img/broken_image.png';
} }
+2 -2
View File
@@ -2,7 +2,7 @@ import React from 'react';
import Chip from './chip.jsx'; import Chip from './chip.jsx';
import { makeImageUrl } from '../lib/blob-helpers.js'; import { makeImageDataUrl } from '../lib/blob-helpers.js';
/* BEGIN ChipInput: group membership widget */ /* BEGIN ChipInput: group membership widget */
export default class ChipInput extends React.Component { export default class ChipInput extends React.Component {
@@ -119,7 +119,7 @@ export default class ChipInput extends React.Component {
chips.push( chips.push(
<Chip <Chip
onCancel={this.handleChipCancel} onCancel={this.handleChipCancel}
avatar={makeImageUrl(item.public ? item.public.photo : null)} avatar={makeImageDataUrl(item.public ? item.public.photo : null)}
title={item.public ? item.public.fn : undefined} title={item.public ? item.public.fn : undefined}
noAvatar={this.props.avatarDisabled} noAvatar={this.props.avatarDisabled}
topic={item.user} topic={item.user}
+3 -3
View File
@@ -9,7 +9,7 @@ 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';
import { makeImageUrl } from '../lib/blob-helpers.js'; import { makeImageDataUrl } from '../lib/blob-helpers.js';
import { MESSAGE_PREVIEW_LENGTH } from '../config.js'; import { MESSAGE_PREVIEW_LENGTH } from '../config.js';
@@ -79,7 +79,7 @@ class ContactList extends React.Component {
let preview; let preview;
let deliveryStatus; let deliveryStatus;
if (!this.props.showMode && c.latestMessage) { if (!this.props.showMode && c.latestMessage) {
const msg = c.latestMessage(); const msg = c.latestMessage(true);
if (msg) { if (msg) {
deliveryStatus = msg._status || c.msgStatus(msg, true); deliveryStatus = msg._status || c.msgStatus(msg, true);
preview = typeof msg.content == 'string' ? preview = typeof msg.content == 'string' ?
@@ -90,7 +90,7 @@ class ContactList extends React.Component {
contactNodes.push( contactNodes.push(
<Contact <Contact
title={c.public ? c.public.fn : null} title={c.public ? c.public.fn : null}
avatar={makeImageUrl(c.public ? c.public.photo : null)} avatar={makeImageDataUrl(c.public ? c.public.photo : null)}
comment={comment} comment={comment}
preview={preview} preview={preview}
received={deliveryStatus} received={deliveryStatus}
+33 -6
View File
@@ -1,6 +1,6 @@
// A single topic or user. // A single topic or user.
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ContactBadges from './contact-badges.jsx'; import ContactBadges from './contact-badges.jsx';
import LetterTile from './letter-tile.jsx'; import LetterTile from './letter-tile.jsx';
@@ -11,7 +11,25 @@ import { Drafty } from 'tinode-sdk';
import { deliveryMarker } from '../lib/utils.js'; import { deliveryMarker } from '../lib/utils.js';
export default class Contact extends React.Component { const messages = defineMessages({
drafty_image: {
id: 'drafty_image',
defaultMessage: 'Picture',
description: 'Comment for embedded images in drafty preview'
},
drafty_form: {
id: 'drafty_form',
defaultMessage: 'Form: ',
description: 'Comment for form in drafty preview'
},
drafty_attachment: {
id: 'drafty_attachment',
defaultMessage: 'Attachment',
description: 'Comment for attachment in drafty preview'
},
});
class Contact extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -65,7 +83,8 @@ export default class Contact extends React.Component {
this.props.comment; this.props.comment;
const icon = deliveryMarker(this.props.received); const icon = deliveryMarker(this.props.received);
const marker = icon ? <i className={'material-icons small space-right ' + icon.color}>{icon.name}</i> : null; const marker = icon ? <i className={'material-icons small space-right' +
(icon.color ? ' ' + icon.color : '')}>{icon.name}</i> : null;
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}>
@@ -105,6 +124,7 @@ function draftyFormatter(style, data, values, key) {
let el = Drafty.tagName(style); let el = Drafty.tagName(style);
const attr = { key: key }; const attr = { key: key };
if (el) { if (el) {
const { formatMessage } = this.props.intl;
switch (style) { switch (style) {
case 'BR': case 'BR':
// Replace new line with a space. // Replace new line with a space.
@@ -122,7 +142,7 @@ function draftyFormatter(style, data, values, key) {
case 'IM': case 'IM':
// Replace image with '[icon] Image'. // Replace image with '[icon] Image'.
el = React.Fragment; el = React.Fragment;
values = [<i key="im" className="material-icons">photo</i>, 'Picture']; values = [<i key="im" className="material-icons">photo</i>, formatMessage(messages.drafty_image)];
break; break;
case 'BN': case 'BN':
el = 'span'; el = 'span';
@@ -130,14 +150,19 @@ function draftyFormatter(style, data, values, key) {
break; break;
case 'FM': case 'FM':
el = React.Fragment; el = React.Fragment;
values = [<i key="fm" className="material-icons">dashboard</i>, 'Form: '].concat(values || []); values = [<i key="fm" className="material-icons">dashboard</i>,
formatMessage(messages.drafty_form)].concat(values || []);
break; break;
case 'RW': case 'RW':
el = React.Fragment; el = React.Fragment;
break; break;
case 'EX': case 'EX':
if (data && data.mime == 'application/json') {
// Ignore JSON attachments: they are form response payloads.
return null;
}
el = React.Fragment; el = React.Fragment;
values = [<i key="ex" className="material-icons">attachment</i>, 'Attachment']; values = [<i key="ex" className="material-icons">attachment</i>, formatMessage(messages.drafty_attachment)];
break; break;
default: default:
if (el == '_UNKN') { if (el == '_UNKN') {
@@ -151,3 +176,5 @@ function draftyFormatter(style, data, values, key) {
return values; return values;
} }
}; };
export default injectIntl(Contact);
+2 -2
View File
@@ -6,7 +6,7 @@ import { FormattedMessage } from 'react-intl';
import { MAX_ONLINE_IN_TOPIC } from '../config.js' import { MAX_ONLINE_IN_TOPIC } from '../config.js'
import LetterTile from './letter-tile.jsx'; import LetterTile from './letter-tile.jsx';
import { makeImageUrl } from '../lib/blob-helpers.js'; import { makeImageDataUrl } from '../lib/blob-helpers.js';
export default class GroupSubs extends React.Component { export default class GroupSubs extends React.Component {
constructor(props) { constructor(props) {
@@ -23,7 +23,7 @@ export default class GroupSubs extends React.Component {
<div className="avatar-box" key={sub.user}> <div className="avatar-box" key={sub.user}>
<LetterTile <LetterTile
topic={sub.user} topic={sub.user}
avatar={makeImageUrl(sub.public ? sub.public.photo : null) || true} avatar={makeImageDataUrl(sub.public ? sub.public.photo : null) || true}
title={sub.public ? sub.public.fn : null} /> title={sub.public ? sub.public.fn : null} />
</div> </div>
); );
+2 -2
View File
@@ -6,7 +6,7 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import CheckBox from './checkbox.jsx'; import CheckBox from './checkbox.jsx';
import Contact from './contact.jsx'; import Contact from './contact.jsx';
import { makeImageUrl } from '../lib/blob-helpers.js'; import { makeImageDataUrl } from '../lib/blob-helpers.js';
// Translatable permission names. // Translatable permission names.
const messages = defineMessages({ const messages = defineMessages({
@@ -135,7 +135,7 @@ class PermissionsEditor extends React.Component {
<ul className="contact-box"><Contact <ul className="contact-box"><Contact
item={this.props.item} item={this.props.item}
title={this.props.userTitle} title={this.props.userTitle}
avatar={makeImageUrl(this.props.userAvatar ? this.props.userAvatar : null)} /></ul> : null} avatar={makeImageDataUrl(this.props.userAvatar ? this.props.userAvatar : null)} /></ul> : null}
<label className="small"><FormattedMessage id="title_permissions" <label className="small"><FormattedMessage id="title_permissions"
defaultMessage="Permissions" description="Section title"/></label> defaultMessage="Permissions" description="Section title"/></label>
<table className="permission-editor"> <table className="permission-editor">
+430 -295
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1311 -1212
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
// This is a generated file. Don't edit.
const PACKAGE_VERSION = "0.17.2";