Compare commits

...
41 Commits
Author SHA1 Message Date
or-else 6a2efac566 and nother place where title length is enforced 2019-11-06 11:27:49 +03:00
or-else 8c1b926008 enforce maximum title length for topics and users 2019-11-06 11:25:07 +03:00
or-else c539e5f70d another Russian translation fix 2019-11-06 11:24:45 +03:00
or-else 154ec5000e rebuilt 2019-11-06 11:03:52 +03:00
or-else 80c5103ce0 UI fix for excessively long topic name 2019-11-06 10:33:21 +03:00
or-else 15c0ff2e75 more idiomatic Russian translation 2019-11-06 10:33:02 +03:00
or-else a404734794 bump version and rebuild 2019-11-03 09:44:28 +03:00
or-else f962c8e8a9 updated dependencies 2019-11-02 09:58:34 +03:00
or-else bb20937bc9 replace unpkg with jsdeliver as unpkg is too unreliable https://github.com/mjackson/unpkg/issues/229 2019-11-01 09:41:06 +03:00
or-else 3026e21b46 updated Missing Functionality section 2019-11-01 09:39:38 +03:00
or-else 92b9faa063 explicitly ignore "upd" presence updates 2019-11-01 09:39:05 +03:00
or-else a1894871aa updated for the next tindroidX package 2019-11-01 09:37:56 +03:00
or-else bd03a7164e fix translation to most common spelling 2019-10-25 08:24:53 +03:00
or-else 9891620188 added "mobile-web-app-capable" meta header 2019-10-23 19:31:59 +03:00
or-else afa0726bdf bump version and rebuild 2019-10-21 09:12:59 +03:00
or-else 1a927e5420 rebuilt with fixed url 2019-10-16 13:05:58 +03:00
Gene 5f4db76cbf Merge pull request #41 from tinode/fix-privacy-link
Fix broken privacy policy link.
2019-10-16 13:04:15 +03:00
aforge 4092324fea Fix broken privacy policy link. 2019-10-15 23:34:56 -07:00
or-else efc7377942 rebuilt with tinode-js fix 2019-10-14 17:01:30 +03:00
or-else 94f88855de rebuilt with tinode-js 2019-10-12 09:40:04 +03:00
or-else 73afafb605 rebuilt because of change in tinode-js: handling of {pres what=term}, tinode/chat#278 2019-10-12 09:00:18 +03:00
or-else fe556a0707 mark tinode.dev.js and tinode.prod.js as binary 2019-10-10 11:59:20 +03:00
or-else 79d0c6dd14 attempted fix for tinode/chat#278 2019-10-10 11:57:23 +03:00
or-else 8e9349b58b split tinode-sdk into a separate file 2019-10-03 18:15:29 +03:00
or-else 22710adbe3 add spinner to login action, small build changes 2019-10-03 16:48:47 +03:00
or-else b8a1b39bba bump version and rebuild 2019-10-02 10:44:59 +03:00
or-else ef341cb94e destructive menu action wrapped in an alert 2019-09-27 16:55:19 +03:00
or-else cda001e5c6 fix: new group topic is immediately unsubscribed 2019-09-26 19:25:18 +03:00
or-else 254c4768eb badges were broken 2019-09-25 14:36:19 +03:00
or-else 8f0125587f add firefox scrollbar styling 2019-09-24 20:50:19 +03:00
or-else 1aaf6f8fab rebuilt after fixing big in deleting messages 2019-09-24 20:04:52 +03:00
or-else 1d56d55487 adding alert component 2019-09-24 19:40:36 +03:00
or-else 205ca95217 bug fix: archive view does not render 2019-09-24 19:26:59 +03:00
or-else f9a29ed3ef add alert for destructable actions 2019-09-24 17:12:38 +03:00
or-else f3420dc66f upgraded react-intl to 3.3 2019-09-23 15:28:22 +03:00
or-else 6d581c8e7c rename chinese translation from zh-CN to zh 2019-09-23 14:15:57 +03:00
or-else 10bb206d51 experimenting with code splitting 2019-09-22 21:32:32 +03:00
or-else a2747b6e33 clarify use of explicit UI human language 2019-09-22 21:32:06 +03:00
or-else b3b0ae59ab updated dependencies 2019-09-22 21:31:33 +03:00
or-else d99434000f clarify group member editing UI (disallow deletes for S only) 2019-09-22 21:30:50 +03:00
or-else 2a164ddf49 links for blocking/reporting users and topics 2019-09-22 21:19:04 +03:00
41 changed files with 9361 additions and 4002 deletions
+2
View File
@@ -3,3 +3,5 @@ index.dev.js.map binary
index.prod.js binary index.prod.js binary
index.prod.js.map binary index.prod.js.map binary
base.min.css binary base.min.css binary
tinode.dev.js binary
tinode.prod.js binary
+4 -4
View File
@@ -2,9 +2,9 @@
"relation": ["delegate_permission/common.handle_all_urls"], "relation": ["delegate_permission/common.handle_all_urls"],
"target": { "target": {
"namespace": "android_app", "namespace": "android_app",
"package_name": "co.tinode.tindroid", "package_name": "co.tinode.tindroidx",
"sha256_cert_fingerprints": "sha256_cert_fingerprints":
["AA:0A:FF:C4:F3:1F:58:83:F3:8E:69:03:2E:B9:AA:1A:C4:62:65:F1:1E:50:F9:7E:AD:35:56:55:32:B7:1A:C6"] ["5B:BE:6A:C4:A7:66:A8:BB:4B:C1:52:66:E5:A4:BD:66:3C:33:50:04:11:CB:17:44:45:89:5E:3C:8D:55:77:7A"]
} }
}, },
{ {
@@ -18,8 +18,8 @@
"relation": ["delegate_permission/common.get_login_creds"], "relation": ["delegate_permission/common.get_login_creds"],
"target": { "target": {
"namespace": "android_app", "namespace": "android_app",
"package_name": "co.tinode.tindroid", "package_name": "co.tinode.tindroidx",
"sha256_cert_fingerprints": "sha256_cert_fingerprints":
["AA:0A:FF:C4:F3:1F:58:83:F3:8E:69:03:2E:B9:AA:1A:C4:62:65:F1:1E:50:F9:7E:AD:35:56:55:32:B7:1A:C6"] ["5B:BE:6A:C4:A7:66:A8:BB:4B:C1:52:66:E5:A4:BD:66:3C:33:50:04:11:CB:17:44:45:89:5E:3C:8D:55:77:7A"]
} }
}] }]
+6 -4
View File
@@ -19,16 +19,18 @@ Try possibly newer or older version live at https://web.tinode.co/. For password
## Internationalization ## Internationalization
The app is fully internationalized using [React-Intl](https://github.com/yahoo/react-intl). The UI language is selected automatically from the language specified by the browser. A specific language can be forced by adding `hl=XX` parameter to the URL, i.e. https://web.tinode.co/#?hl=ru. The app is fully internationalized using [React-Intl](https://github.com/formatjs/react-intl). The UI language is selected automatically from the language specified by the browser. A specific language can be forced by adding `#?hl=XX` parameter to the URL, i.e. https://web.tinode.co/#?hl=ru.
As of the time of this writing the following translations exist: English, Russian, Simplified Chinese. More translations are welcome. Send a pull request with a json file with translated strings. Take a look at [English](/src/i18n/en.json), [Russian](/src/i18n/ru.json), or [Simplified Chinese](/src/i18n/zh-CN.json) translations for guidance. As of the time of this writing the following translations exist: English, Russian, Simplified Chinese. More translations are welcome. Send a pull request with a json file with translated strings. Take a look at [English](/src/i18n/en.json), [Russian](/src/i18n/ru.json), or [Simplified Chinese](/src/i18n/zh-CN.json) translations for guidance.
## Missing functionality ## Not done yet
* Previews not generated for videos, audio, links or docs. * End-to-End encryption.
* No end-to-end encryption.
* Emoji support is weak. * Emoji support is weak.
* Mentions, hashtags.
* Replying or forwarding messages.
* Previews not generated for videos, audio, links or docs.
## Other ## Other
+99 -46
View File
@@ -9,9 +9,9 @@
/* Basic elements defined */ /* Basic elements defined */
html, body { html, body {
height:100%; height: 100%;
width:100%; width: 100%;
font-size: 10pt; font-size: 10pt; /* 13.33 px/rem */
color: #666; color: #666;
overflow: hidden; overflow: hidden;
background-color: silver; background-color: silver;
@@ -82,9 +82,9 @@ img.avatar {
button { button {
display: inline-block; display: inline-block;
font-family: inherit; font-family: inherit;
font-size: 1.1rem; font-size: 1.05rem;
height: 2.2em; height: 2.2rem;
line-height: 2.2em; line-height: 2.2rem;
font-weight: normal; font-weight: normal;
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
@@ -92,11 +92,13 @@ button {
cursor: pointer; cursor: pointer;
border: 1px solid transparent; border: 1px solid transparent;
white-space: nowrap; white-space: nowrap;
padding-left: 2rem; padding-left: 1.2rem;
padding-right: 2rem; padding-right: 1.2rem;
border-radius: 2px; border-radius: 4px;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.25); box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.25);
text-transform: uppercase; text-transform: uppercase;
min-width: 5rem;
margin: 0 0.35rem 0 0.35rem;
} }
button[disabled], button[disabled],
@@ -148,7 +150,7 @@ button.blue {
color: white; color: white;
} }
button.blue:focus, button.blue:hover, button.blue:active { button.blue:focus, button.blue:hover, button.blue:active {
background-color: #1976d2; background-color: #2962ff;
} }
button.white { button.white {
@@ -159,6 +161,16 @@ button.white:focus, button.white:hover, button.blue:active {
background-color: #e3f2fd; background-color: #e3f2fd;
} }
button.outline {
background-color: transparent;
color: #2196f3; /* blue 500 */
border: 1px solid silver;
box-shadow: none;
}
button.outline:focus, button.outline:hover, button.outline:active {
background-color: #e8eaf6; /* indigo 50 */
}
textarea, input { textarea, input {
font-family: inherit; font-family: inherit;
font-size: 1.1rem; font-size: 1.1rem;
@@ -222,8 +234,11 @@ input[type="password"].with-visibility {
margin-right:-2rem; margin-right:-2rem;
padding-right:2rem; padding-right:2rem;
} }
/* END of input and buttons */
/* Scrollbar styling for Chrome. Firefox does not support scrollbar styling. */ /* BEGIN scrollbar styling. */
/* Chrome & other webkit browsers */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 0.6rem; width: 0.6rem;
} }
@@ -237,7 +252,12 @@ input[type="password"].with-visibility {
background-color: rgba(0,0,0,0.3); background-color: rgba(0,0,0,0.3);
} }
/* END of input and buttons */ /* Firefox only */
* {
scrollbar-width: thin;
scrollbar-color: silver;
}
/* END of Scrollbar styling. */
/* BEGIN Popup/dropdown menu */ /* BEGIN Popup/dropdown menu */
@@ -303,6 +323,40 @@ li.right:hover .menuTrigger {
/* END popup/dropdown menu */ /* END popup/dropdown menu */
/* Begin Alert: modal window with message and buttons. */
div.alert-container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
position: absolute;
-x-backdrop-filter: blur(4px);
background: rgba(0,0,0,0.33);
z-index: 3;
}
div.alert {
background-color: white;
box-shadow: 0 1px 6px rgba(0, 0, 0, .45);
padding: 1rem;
border-radius: 4px;
margin: auto 2rem auto 2rem;
width: content;
min-width: 16rem;
max-width: 24rem;
}
div.alert .title {
color: #2196f3;
font-size: 1.2rem;
font-weight: 500;
width: 100%;
padding: 0 0 0.35rem 0;
}
div.alert .content {
padding: 0.5rem 0 0.5rem 0;
}
/* END Alert */
/* Generic text colors */ /* Generic text colors */
.blue { .blue {
color: #2196f3; /* blue 500 */ color: #2196f3; /* blue 500 */
@@ -354,7 +408,7 @@ li.right:hover .menuTrigger {
} }
/* Top-level application layout style: sidepanel + main view */ /* Top-level application layout style: sidepanel + main view */
#noscript-alertbox { #noscript-message {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -405,6 +459,8 @@ li.right:hover .menuTrigger {
} }
#topic-view { #topic-view {
position: relative;
overflow-x: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
@@ -552,6 +608,7 @@ table.permission-editor td.checkbox {
height: 4rem; height: 4rem;
white-space: nowrap; white-space: nowrap;
box-shadow: 0 1px 2px gray; box-shadow: 0 1px 2px gray;
overflow-x: hidden;
z-index: 1; /* otherwise the shadow becomes invisible */ z-index: 1; /* otherwise the shadow becomes invisible */
} }
@@ -659,57 +716,57 @@ i.search {
} }
/* Area which displays an error message */ /* Area which displays an error message */
.alert-box { .info-box {
position: relative; position: relative;
display: none; display: none;
overflow: hidden; overflow: hidden;
padding: 1rem 1.5rem 1rem 0.5rem; padding: 1rem 1.5rem 1rem 0.5rem;
} }
.alert-box .cancel { .info-box .cancel {
position: absolute; position: absolute;
right: 0.25rem; right: 0.25rem;
top: 0.5rem; top: 0.5rem;
} }
.alert-box.error { .info-box.error {
display: flex; display: flex;
background-color: #ffcdd2; background-color: #ffcdd2;
border-bottom: 1px solid #ffbfc8; border-bottom: 1px solid #ffbfc8;
color: #b71c1c; color: #b71c1c;
} }
.alert-box.warning { .info-box.warning {
display: flex; display: flex;
background-color: #fff9c4; background-color: #fff9c4;
border-bottom: 1px solid #fff59d; border-bottom: 1px solid #fff59d;
color: #6d4c41; color: #6d4c41;
} }
.alert-box.info { .info-box.info {
display: flex; display: flex;
background-color: #e1f5fe; background-color: #e1f5fe;
border-bottom: 1px solid #b3e5fc; border-bottom: 1px solid #b3e5fc;
color: #0288d1; color: #0288d1;
} }
.alert-box .cancel .material-icons { .info-box .cancel .material-icons {
font-size: 1rem; font-size: 1rem;
color: #757575; color: #757575;
} }
.alert-box.error .cancel .material-icons { .info-box.error .cancel .material-icons {
color: #b71c1c; color: #b71c1c;
} }
.alert-box .icon { .info-box .icon {
display: block; display: block;
margin-right: 0.5rem; margin-right: 0.5rem;
} }
.alert-box .icon .material-icons { .info-box .icon .material-icons {
font-size: 1.5rem; font-size: 1.5rem;
} }
.alert-box.warning .icon .material-icons { .info-box.warning .icon .material-icons {
color: #fbc02d; color: #fbc02d;
} }
.alert-box.error .icon .material-icons { .info-box.error .icon .material-icons {
color: #ef5350; color: #ef5350;
} }
.alert-box.info .icon .material-icons { .info-box.info .icon .material-icons {
color: #0288d1; color: #0288d1;
} }
/* List of contacts */ /* List of contacts */
@@ -876,7 +933,6 @@ i.search {
.panel-form-row { .panel-form-row {
display: flex; display: flex;
justify-content: flex-start;
align-items: center; align-items: center;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
line-height: 1.5; line-height: 1.5;
@@ -901,8 +957,7 @@ i.search {
flex-grow: 1; flex-grow: 1;
} }
.panel-form-column > .panel-form-row, .panel-form-column > .panel-form-row,
.panel-form-row > .panel-form-row .panel-form-row > .panel-form-row {
{
padding: 0!important; padding: 0!important;
} }
@@ -1014,10 +1069,6 @@ i.search {
/* END of sidepanel styles */ /* END of sidepanel styles */
/* Topic/messages view */ /* Topic/messages view */
#topic-view {
position: relative;
}
#topic-caption-panel { #topic-caption-panel {
background-color: #b0bec5; background-color: #b0bec5;
} }
@@ -1044,6 +1095,10 @@ i.search {
line-height: 3rem; line-height: 3rem;
} }
#topic-title-group {
overflow-x: hidden;
}
#hide-message-view { #hide-message-view {
display: block; display: block;
margin-right: 0.35rem; margin-right: 0.35rem;
@@ -1560,45 +1615,43 @@ ul.tabbar li.active > a:after {
/* Badge with a label, like [you], [muted], etc */ /* Badge with a label, like [you], [muted], etc */
.badge { .badge {
font-family: 'Roboto Mono', Courier, monospace, sans-serif; font-family: 'Roboto Mono', Courier, monospace, sans-serif;
font-size: 0.75rem; font-size: 0.8rem;
border: 1px solid #666; border: 1px solid silver;
background-color: #eee; background-color: #e8e8e8;
color: #666; color: #666;
padding: 0 0.15rem 0 0.15rem; padding: 0.05rem 0.25rem 0.05rem 0.25rem;
margin-left: 0.1rem; margin: 0.05rem 0.1rem 0.05rem 0.1rem;
margin-right: 0.1rem; border-radius: 0.2rem;
margin-bottom: 0.1rem;
border-radius: 0.15rem;
display: inline-block; display: inline-block;
align-self: flex-start; align-self: flex-start;
} }
.badge.green { .badge.green {
border-color: #383; border-color: #9c9;
background-color: #efe; background-color: #efe;
color: #383; color: #383;
} }
.badge.yellow { .badge.yellow {
border-color: #660; border-color: #cc9;
background-color: #ffe; background-color: #ffe;
color: #660; color: #660;
} }
.badge.blue { .badge.blue {
border-color: #339; border-color: #99c;
background-color: #eef; background-color: #eef;
color: #339; color: #339;
} }
.badge.red { .badge.red {
border-color: #933; border-color: #c99;
background-color: #fee; background-color: #fee;
color: #933; color: #933;
} }
.badge.magenta { .badge.magenta {
border-color: #939; border-color: #c9c;
background-color: #fef; background-color: #fef;
color: #939; color: #939;
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 46 KiB

+12 -11
View File
@@ -16,7 +16,8 @@
<meta name="og:url" content="https://web.tinode.co/"> <meta name="og:url" content="https://web.tinode.co/">
<meta name="og:title" content="Tinode Web Development"> <meta name="og:title" content="Tinode Web Development">
<meta name="og:image" content="img/og-logo.jpeg"> <meta name="og:image" content="img/og-logo.jpeg">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<link rel="manifest" href="manifest.json" /> <link rel="manifest" href="manifest.json" />
<link rel="shortcut icon" id="shortcut-icon" href="img/logo32x32.png" type="image/png" /> <link rel="shortcut icon" id="shortcut-icon" href="img/logo32x32.png" type="image/png" />
<link rel="apple-touch-icon" sizes="192x192" href="img/logo192.png" type="image/png" /> <link rel="apple-touch-icon" sizes="192x192" href="img/logo192.png" type="image/png" />
@@ -27,19 +28,18 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" /> <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<!-- Styles for local elements --> <!-- Styles for local elements -->
<link rel="stylesheet" href="css/base.css" /> <link rel="stylesheet" href="css/base.css" />
<!-- ReactJs scripts --> <!-- ReactJS -->
<script crossorigin="anonymous" src="https://unpkg.com/react@16/umd/react.development.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@16/umd/react.development.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@16/umd/react-dom.development.js"></script>
<!-- React-Intl --> <!-- React-Intl for translations -->
<script crossorigin="anonymous" src="https://unpkg.com/prop-types@15/prop-types.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@3/dist/react-intl.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/react-intl@2/dist/react-intl.js"></script> <!-- Firebase for push notifications -->
<!-- Firebase scripts --> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@6.6.2/firebase-app.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/firebase@6.4.2/firebase-app.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@6.6.2/firebase-messaging.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/firebase@6.4.2/firebase-messaging.js"></script>
</head> </head>
<body> <body>
<noscript><div id="noscript-alertbox"><div id="noscript-title">Javascript support is required.</div> <noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
<div id="noscript-content">It appears JavaScript is turned off or is not supported by your browser. <div id="noscript-content">It appears JavaScript is turned off or is not supported by your browser.
TinodeWeb does not work without JavaScript. Please enable JavaScript by changing your browser options, TinodeWeb does not work without JavaScript. Please enable JavaScript by changing your browser options,
then <a href="">try again</a>. then <a href="">try again</a>.
@@ -47,6 +47,7 @@
</div></noscript> </div></noscript>
<div id="mountPoint"></div> <div id="mountPoint"></div>
<script src="firebase-init.js"></script> <script src="firebase-init.js"></script>
<script src="umd/tinode.dev.js"></script>
<script src="umd/index.dev.js"></script> <script src="umd/index.dev.js"></script>
</body> </body>
</html> </html>
+9 -7
View File
@@ -13,6 +13,8 @@
<meta name="og:url" content="https://web.tinode.co/"> <meta name="og:url" content="https://web.tinode.co/">
<meta name="og:title" content="Tinode Web"> <meta name="og:title" content="Tinode Web">
<meta name="og:image" content="img/og-logo.jpeg"> <meta name="og:image" content="img/og-logo.jpeg">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<link rel="manifest" href="manifest.json" /> <link rel="manifest" href="manifest.json" />
<link rel="shortcut icon" id="shortcut-icon" href="img/logo32x32.png" type="image/png" /> <link rel="shortcut icon" id="shortcut-icon" href="img/logo32x32.png" type="image/png" />
<link rel="apple-touch-icon" sizes="192x192" href="img/logo192.png" type="image/png" /> <link rel="apple-touch-icon" sizes="192x192" href="img/logo192.png" type="image/png" />
@@ -20,20 +22,20 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,400i,500,700,700i" /> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,400i,500,700,700i" />
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" /> <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<link rel="stylesheet" href="css/base.min.css" /> <link rel="stylesheet" href="css/base.min.css" />
<script crossorigin="anonymous" src="https://unpkg.com/react@16.9.0/umd/react.production.min.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@16.11.0/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.9.0/umd/react-dom.production.min.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@16.11.0/umd/react-dom.production.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/prop-types@15.7.2/prop-types.min.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@3.4.0/dist/react-intl.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/react-intl@2.9.0/dist/react-intl.min.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@6.6.2/firebase-app.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/firebase@6.4.2/firebase-app.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@6.6.2/firebase-messaging.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/firebase@6.4.2/firebase-messaging.js"></script>
</head> </head>
<body> <body>
<noscript><div id="noscript-alertbox"><div id="noscript-title">Javascript support is required.</div> <noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
<div id="noscript-content">It appears JavaScript is turned off or is not supported by your browser. <div id="noscript-content">It appears JavaScript is turned off or is not supported by your browser.
TinodeWeb does not work without JavaScript. Please enable JavaScript by changing your browser options, TinodeWeb does not work without JavaScript. Please enable JavaScript by changing your browser options,
then <a href="">try again</a>.</div></div></noscript> then <a href="">try again</a>.</div></div></noscript>
<div id="mountPoint"></div> <div id="mountPoint"></div>
<script src="firebase-init.js"></script> <script src="firebase-init.js"></script>
<script src="umd/tinode.prod.js"></script>
<script src="umd/index.prod.js"></script> <script src="umd/index.prod.js"></script>
</body> </body>
</html> </html>
+923 -605
View File
File diff suppressed because it is too large Load Diff
+17 -13
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.16.1", "version": "0.16.2-rc4",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/tinode/example-react-js.git" "url": "https://github.com/tinode/example-react-js.git"
@@ -9,11 +9,12 @@
"scripts": { "scripts": {
"build": "npm run vers && npm run build:prod && npm run build:dev && npm run build:css && npm run build:i18n", "build": "npm run vers && npm run build:prod && npm run build:dev && npm run build:css && npm run build:i18n",
"build:css": "postcss css/base.css > css/base.min.css", "build:css": "postcss css/base.css > css/base.min.css",
"build:dev": "webpack --mode development --output ./umd/index.dev.js", "build:dev": "webpack --mode development",
"build:prod": "webpack --mode production --output ./umd/index.prod.js", "build:prod": "webpack --mode production",
"build:i18n": "node scripts/flatten-messages.js", "build:i18n": "node scripts/flatten-messages.js",
"vers": "node scripts/gen-version.js" "vers": "node scripts/gen-version.js"
}, },
"browserslist": "> 0.5%, not IE 11",
"files": [ "files": [
"src/index.js", "src/index.js",
"src/config.js", "src/config.js",
@@ -25,6 +26,8 @@
"umd/index.prod.js.map", "umd/index.prod.js.map",
"umd/index.dev.js", "umd/index.dev.js",
"umd/index.dev.js.map", "umd/index.dev.js.map",
"umd/tinode.prod.js",
"umd/tinode.dev.js",
"audio/msg.mp3", "audio/msg.mp3",
"css/base.css", "css/base.css",
"css/base.min.css", "css/base.min.css",
@@ -68,20 +71,21 @@
"firebase": "^6.4.2", "firebase": "^6.4.2",
"react": "^16.9.0", "react": "^16.9.0",
"react-dom": "^16.9.0", "react-dom": "^16.9.0",
"react-intl": "^2.9.0", "react-intl": "^3.2.1",
"tinode-sdk": "^0.16.1" "tinode-sdk": "^0.16.2-rc3"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.5.4", "@babel/core": "^7.6.0",
"@babel/preset-env": "^7.5.4", "@babel/preset-env": "^7.6.0",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"babel-plugin-react-intl": "^4.1.0", "babel-plugin-react-intl": "^4.1.19",
"browserslist": "^4.6.6", "browserslist": "^4.7.0",
"copy-webpack-plugin": "^5.0.4",
"cssnano": "^4.1.10", "cssnano": "^4.1.10",
"postcss": "^7.0.17", "postcss": "^7.0.18",
"uglifyjs-webpack-plugin": "^2.1.3", "uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "^4.35.3", "webpack": "^4.40.2",
"webpack-cli": "^3.3.6" "webpack-cli": "^3.3.9"
} }
} }
+2 -2
View File
@@ -1,6 +1,6 @@
// Must be located at the root. // Must be located at the root.
importScripts('https://unpkg.com/firebase@6.4.2/firebase-app.js'); importScripts('https://cdn.jsdelivr.net/npm/firebase@6.6.2/firebase-app.js');
importScripts('https://unpkg.com/firebase@6.4.2/firebase-messaging.js'); importScripts('https://cdn.jsdelivr.net/npm/firebase@6.6.2/firebase-messaging.js');
importScripts('firebase-init.js'); importScripts('firebase-init.js');
firebase.initializeApp(FIREBASE_INIT); firebase.initializeApp(FIREBASE_INIT);
+11 -3
View File
@@ -12,6 +12,9 @@ export const KNOWN_HOSTS = {hosted: 'web.tinode.co', local: 'localhost:6060'};
// Default host name and port to connect to. // Default host name and port to connect to.
export const DEFAULT_HOST = KNOWN_HOSTS.hosted; export const DEFAULT_HOST = KNOWN_HOSTS.hosted;
// Enable console logging.
export const LOGGING_ENABLED = true;
// Minimum time between two keypress notifications, milliseconds. // Minimum time between two keypress notifications, milliseconds.
export const KEYPRESS_DELAY = 3*1000; export const KEYPRESS_DELAY = 3*1000;
// Delay before sending a {note} for reciving a message, milliseconds. // Delay before sending a {note} for reciving a message, milliseconds.
@@ -22,8 +25,10 @@ export const READ_DELAY = 1000;
// The shortest allowed tag length. Matches the value on the server. // The shortest allowed tag length. Matches the value on the server.
export const MIN_TAG_LENGTH = 4; export const MIN_TAG_LENGTH = 4;
// Access mode for subscriptions initiated by the current user. // Access mode for P2P subscriptions initiated by the current user.
export const DEFAULT_ACCESS_MODE = 'JRWPS'; export const DEFAULT_P2P_ACCESS_MODE = 'JRWPS';
// Access mode for new group topics created by the current user.
export const NEW_GRP_ACCESS_MODE = 'JRWPSAO';
// Access mode for no access. // Access mode for no access.
export const NO_ACCESS_MODE = 'N'; export const NO_ACCESS_MODE = 'N';
@@ -64,11 +69,14 @@ export const MAX_IMAGE_DIM = 768;
// hidden under "+X more" // hidden under "+X more"
export const MAX_ONLINE_IN_TOPIC = 4; export const MAX_ONLINE_IN_TOPIC = 4;
// Maximum length of topic title (and Private comment) or user name.
export const MAX_TITLE_LENGTH = 40;
// Link for "Contact Us". // Link for "Contact Us".
export const LINK_CONTACT_US = 'email:info@tinode.co'; export const LINK_CONTACT_US = 'email:info@tinode.co';
// Link to Privacy Policy. // Link to Privacy Policy.
export const LINK_PRIVACY_POLICY = 'https://tinode.co/privacy.htnl'; export const LINK_PRIVACY_POLICY = 'https://tinode.co/privacy.html';
// Link to Terms of Service. // Link to Terms of Service.
export const LINK_TERMS_OF_SERVICE = 'https://tinode.co/terms.html'; export const LINK_TERMS_OF_SERVICE = 'https://tinode.co/terms.html';
+12 -2
View File
@@ -24,6 +24,16 @@
"granted_permissions": "Granted", "granted_permissions": "Granted",
"menu_item_edit_permissions": "Edit permissions", "menu_item_edit_permissions": "Edit permissions",
"label_other_user": "Other", "label_other_user": "Other",
"action_clear_messages": "Clear Messages",
"clear_messages_warning": "Are you sure you want to clear all messages? It cannot be undone.",
"action_delete_messages": "Clear Messages for All",
"delete_messages_warning": "Are you sure you want to delete all messages for everyone? It cannot be undone.",
"action_leave_chat": "Leave Conversation",
"leave_chat_warning": "Are you sure you want to leave this conversation?",
"action_block_contact": "Block Contact",
"block_contact_warning": "Are you sure you want to block this contact?",
"action_report_chat": "Report Conversation",
"report_chat_warning": "Are you sure you want to block and report this conversation?",
"title_info": "Info", "title_info": "Info",
"label_topic_name": "Name", "label_topic_name": "Name",
"label_private": "Private comment", "label_private": "Private comment",
@@ -36,9 +46,7 @@
"label_default_access": "Default access mode:", "label_default_access": "Default access mode:",
"label_group_members": "Group members:", "label_group_members": "Group members:",
"button_add_members": "Add members", "button_add_members": "Add members",
"button_leave": "Leave",
"group_has_no_members": "No members", "group_has_no_members": "No members",
"action_leave_chat": "Leave",
"login_prompt": "Login", "login_prompt": "Login",
"password_prompt": "Password", "password_prompt": "Password",
"stay_logged_in": "Stay logged in", "stay_logged_in": "Stay logged in",
@@ -94,8 +102,10 @@
"menu_item_mute": "Mute", "menu_item_mute": "Mute",
"menu_item_unmute": "Unmute", "menu_item_unmute": "Unmute",
"menu_item_delete_topic": "Delete", "menu_item_delete_topic": "Delete",
"topic_delete_warning": "Are you sure you want to delete this conversation?",
"menu_item_unblock": "Unblock", "menu_item_unblock": "Unblock",
"menu_item_block": "Block", "menu_item_block": "Block",
"topic_block_warning": "Are you sure you want to block this conversation?",
"menu_item_member_delete": "Remove", "menu_item_member_delete": "Remove",
"menu_item_archive_topic": "Archive", "menu_item_archive_topic": "Archive",
"action_cancel": "cancel", "action_cancel": "cancel",
+17 -7
View File
@@ -2,7 +2,7 @@
"contacts_not_found": "Чатов нет<br />¯∖_(ツ)_/¯", "contacts_not_found": "Чатов нет<br />¯∖_(ツ)_/¯",
"full_name_prompt": "Полное имя, напр. Иван Петров", "full_name_prompt": "Полное имя, напр. Иван Петров",
"email_prompt": "Email, напр. ivan@example.com", "email_prompt": "Email, напр. ivan@example.com",
"button_sign_up": "Создать счет", "button_sign_up": "Создать аккаунт",
"label_your_name": "Ваше имя", "label_your_name": "Ваше имя",
"label_password": "Пароль", "label_password": "Пароль",
"password_unchanged_prompt": "Не изменен", "password_unchanged_prompt": "Не изменен",
@@ -11,7 +11,7 @@
"label_message_sound": "Звук нового сообщения:", "label_message_sound": "Звук нового сообщения:",
"label_push_notifications": "Уведомления:", "label_push_notifications": "Уведомления:",
"label_push_notifications_disabled": "Уведомления (требуют HTTPS):", "label_push_notifications_disabled": "Уведомления (требуют HTTPS):",
"title_tag_manager": "Таги для поиска", "title_tag_manager": "Теги для поиска",
"button_logout": "Выйти", "button_logout": "Выйти",
"login_prompt": "Логин", "login_prompt": "Логин",
"password_prompt": "Пароль", "password_prompt": "Пароль",
@@ -34,12 +34,12 @@
"sidepanel_title_login": "Авторизация", "sidepanel_title_login": "Авторизация",
"sidepanel_title_register": "Зарегистрироваться", "sidepanel_title_register": "Зарегистрироваться",
"sidepanel_title_settings": "Настройки", "sidepanel_title_settings": "Настройки",
"sidepanel_title_edit_account": "Редактировать счет", "sidepanel_title_edit_account": "Редактировать аккаунт",
"sidepanel_title_newtpk": "Новый чат", "sidepanel_title_newtpk": "Новый чат",
"sidepanel_title_cred": "Подтвердить", "sidepanel_title_cred": "Подтвердить",
"sidepanel_title_reset": "Сменить пароль", "sidepanel_title_reset": "Сменить пароль",
"tags_not_found": "Тагов нет. Добавьте", "tags_not_found": "Тегов нет. Добавьте",
"tags_editor_no_tags": "Добавьте таги", "tags_editor_no_tags": "Добавьте теги",
"title_manage_tags": "Редактировать", "title_manage_tags": "Редактировать",
"message_sending": "в пути...", "message_sending": "в пути...",
"message_sending_failed": "ошибка", "message_sending_failed": "ошибка",
@@ -86,7 +86,6 @@
"label_default_access": "Права по умолчанию:", "label_default_access": "Права по умолчанию:",
"label_group_members": "Участники чата:", "label_group_members": "Участники чата:",
"button_add_members": "Добавить", "button_add_members": "Добавить",
"button_leave": "Отписаться",
"group_has_no_members": "Нет участников", "group_has_no_members": "Нет участников",
"action_leave_chat": "Уйти из чата", "action_leave_chat": "Уйти из чата",
"menu_item_info": "Информация", "menu_item_info": "Информация",
@@ -138,5 +137,16 @@
"label_legal": "Прочее", "label_legal": "Прочее",
"link_contact_us": "Связаться с нами", "link_contact_us": "Связаться с нами",
"link_terms_of_service": "Условия сервиса", "link_terms_of_service": "Условия сервиса",
"link_privacy_policy": "Политика конфиденциальности" "link_privacy_policy": "Политика конфиденциальности",
"action_delete_messages": "Удалить сообщения",
"action_block_contact": "Заблокировать контакт",
"action_report_chat": "Сообщить о нарушении",
"delete_messages_warning": "Вы действительно хотите удалить все сообщения?",
"leave_chat_warning": "Вы действительно хотите покинуть этот чат?",
"block_contact_warning": "Вы действительно заблокировать этот контакт?",
"report_chat_warning": "Вы действительно хотите сообщить о нарушении и заблокировать этот чат?",
"action_clear_messages": "Удалить сообщения",
"clear_messages_warning": "Вы действительно хотите удалить все сообщения в чате? Их будет невозможно восстановить.",
"topic_delete_warning": ":!![MISSING]!!:Are you sure you want to delete this conversation?",
"topic_block_warning": ":!![MISSING]!!:Are you sure you want to block this conversation?"
} }
+12 -2
View File
@@ -30,7 +30,6 @@
"label_default_access": "默认权限模式:", "label_default_access": "默认权限模式:",
"label_group_members": "群组成员:", "label_group_members": "群组成员:",
"button_add_members": "添加成员", "button_add_members": "添加成员",
"button_leave": "离开",
"group_has_no_members": "无成员", "group_has_no_members": "无成员",
"action_leave_chat": "离开", "action_leave_chat": "离开",
"login_prompt": "登录", "login_prompt": "登录",
@@ -138,5 +137,16 @@
"label_legal": "法律东西", "label_legal": "法律东西",
"link_contact_us": "联系我们", "link_contact_us": "联系我们",
"link_terms_of_service": "条款和条件", "link_terms_of_service": "条款和条件",
"link_privacy_policy": "隐私政策" "link_privacy_policy": "隐私政策",
"action_delete_messages": "删除所有帖子",
"action_block_contact": "屏蔽联系人",
"delete_messages_warning": ":!![MISSING]!!:Are you sure you want to delete all mesages? It cannot be undone.",
"leave_chat_warning": ":!![MISSING]!!:Are you sure you want to leave this conversation.",
"block_contact_warning": ":!![MISSING]!!:Are you sure you want to block this contact?",
"action_report_chat": "检举垃圾邮件",
"report_chat_warning": ":!![MISSING]!!:Are you sure you want to block and report this conversation?",
"action_clear_messages": ":!![MISSING]!!:Clear Messages",
"clear_messages_warning": ":!![MISSING]!!:Are you sure you want to clear all messages? It cannot be undone.",
"topic_delete_warning": ":!![MISSING]!!:Are you sure you want to delete this conversation?",
"topic_block_warning": ":!![MISSING]!!:Are you sure you want to block this conversation?"
} }
+11 -9
View File
@@ -3,24 +3,26 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { IntlProvider, addLocaleData } from 'react-intl';
import ruData from 'react-intl/locale-data/ru'; import { IntlProvider } from 'react-intl';
addLocaleData(ruData);
import allMessages from './messages.json'; import allMessages from './messages.json';
import TinodeWeb from './views/tinode-web.jsx'; import TinodeWeb from './views/tinode-web.jsx';
import HashNavigation from './lib/navigation.js'; import HashNavigation from './lib/navigation.js';
const {params} = HashNavigation.parseUrlHash(window.location.hash); // Detect human language to use in the UI:
// Check parameters from URL hash #?hl=ru, then browser, then use 'en' as a fallback.
const { params } = HashNavigation.parseUrlHash(window.location.hash);
const language = (params && params.hl) || const language = (params && params.hl) ||
(navigator.languages && navigator.languages[0]) || (navigator.languages && navigator.languages[0]) ||
navigator.language || navigator.language ||
navigator.userLanguage; navigator.userLanguage ||
'en';
const baseLanguage = language.toLowerCase().split(/[-_]+/)[0]; // Get the base language 'en' from a more specific 'en_GB' or 'en-US' as a partial fallback.
const baseLanguage = language.toLowerCase().split(/[-_]/)[0];
// Try full locale, try locale without region code, fallback to 'en' // Try the full locale first, then the locale without the region code, fallback to 'en'.
const messages = const messages =
allMessages[language] || allMessages[language] ||
allMessages[baseLanguage] || allMessages[baseLanguage] ||
+30 -10
View File
@@ -25,6 +25,16 @@
"granted_permissions": "Granted", "granted_permissions": "Granted",
"menu_item_edit_permissions": "Edit permissions", "menu_item_edit_permissions": "Edit permissions",
"label_other_user": "Other", "label_other_user": "Other",
"action_clear_messages": "Clear Messages",
"clear_messages_warning": "Are you sure you want to clear all messages? It cannot be undone.",
"action_delete_messages": "Clear Messages for All",
"delete_messages_warning": "Are you sure you want to delete all messages for everyone? It cannot be undone.",
"action_leave_chat": "Leave Conversation",
"leave_chat_warning": "Are you sure you want to leave this conversation?",
"action_block_contact": "Block Contact",
"block_contact_warning": "Are you sure you want to block this contact?",
"action_report_chat": "Report Conversation",
"report_chat_warning": "Are you sure you want to block and report this conversation?",
"title_info": "Info", "title_info": "Info",
"label_topic_name": "Name", "label_topic_name": "Name",
"label_private": "Private comment", "label_private": "Private comment",
@@ -37,9 +47,7 @@
"label_default_access": "Default access mode:", "label_default_access": "Default access mode:",
"label_group_members": "Group members:", "label_group_members": "Group members:",
"button_add_members": "Add members", "button_add_members": "Add members",
"button_leave": "Leave",
"group_has_no_members": "No members", "group_has_no_members": "No members",
"action_leave_chat": "Leave",
"login_prompt": "Login", "login_prompt": "Login",
"password_prompt": "Password", "password_prompt": "Password",
"stay_logged_in": "Stay logged in", "stay_logged_in": "Stay logged in",
@@ -95,8 +103,10 @@
"menu_item_mute": "Mute", "menu_item_mute": "Mute",
"menu_item_unmute": "Unmute", "menu_item_unmute": "Unmute",
"menu_item_delete_topic": "Delete", "menu_item_delete_topic": "Delete",
"topic_delete_warning": "Are you sure you want to delete this conversation?",
"menu_item_unblock": "Unblock", "menu_item_unblock": "Unblock",
"menu_item_block": "Block", "menu_item_block": "Block",
"topic_block_warning": "Are you sure you want to block this conversation?",
"menu_item_member_delete": "Remove", "menu_item_member_delete": "Remove",
"menu_item_archive_topic": "Archive", "menu_item_archive_topic": "Archive",
"action_cancel": "cancel", "action_cancel": "cancel",
@@ -154,7 +164,7 @@
"label_message_sound": "Звук нового сообщения:", "label_message_sound": "Звук нового сообщения:",
"label_push_notifications": "Уведомления:", "label_push_notifications": "Уведомления:",
"label_push_notifications_disabled": "Уведомления (требуют HTTPS):", "label_push_notifications_disabled": "Уведомления (требуют HTTPS):",
"title_tag_manager": "Таги для поиска", "title_tag_manager": "Теги для поиска",
"button_logout": "Выйти", "button_logout": "Выйти",
"login_prompt": "Логин", "login_prompt": "Логин",
"password_prompt": "Пароль", "password_prompt": "Пароль",
@@ -181,8 +191,8 @@
"sidepanel_title_newtpk": "Новый чат", "sidepanel_title_newtpk": "Новый чат",
"sidepanel_title_cred": "Подтвердить", "sidepanel_title_cred": "Подтвердить",
"sidepanel_title_reset": "Сменить пароль", "sidepanel_title_reset": "Сменить пароль",
"tags_not_found": "Тагов нет. Добавьте", "tags_not_found": "Тегов нет. Добавьте",
"tags_editor_no_tags": "Добавьте таги", "tags_editor_no_tags": "Добавьте теги",
"title_manage_tags": "Редактировать", "title_manage_tags": "Редактировать",
"message_sending": "в пути...", "message_sending": "в пути...",
"message_sending_failed": "ошибка", "message_sending_failed": "ошибка",
@@ -229,7 +239,6 @@
"label_default_access": "Права по умолчанию:", "label_default_access": "Права по умолчанию:",
"label_group_members": "Участники чата:", "label_group_members": "Участники чата:",
"button_add_members": "Добавить", "button_add_members": "Добавить",
"button_leave": "Отписаться",
"group_has_no_members": "Нет участников", "group_has_no_members": "Нет участников",
"action_leave_chat": "Уйти из чата", "action_leave_chat": "Уйти из чата",
"menu_item_info": "Информация", "menu_item_info": "Информация",
@@ -281,9 +290,18 @@
"label_legal": "Прочее", "label_legal": "Прочее",
"link_contact_us": "Связаться с нами", "link_contact_us": "Связаться с нами",
"link_terms_of_service": "Условия сервиса", "link_terms_of_service": "Условия сервиса",
"link_privacy_policy": "Политика конфиденциальности" "link_privacy_policy": "Политика конфиденциальности",
"action_delete_messages": "Удалить сообщения",
"action_block_contact": "Заблокировать контакт",
"action_report_chat": "Сообщить о нарушении",
"delete_messages_warning": "Вы действительно хотите удалить все сообщения?",
"leave_chat_warning": "Вы действительно хотите покинуть этот чат?",
"block_contact_warning": "Вы действительно заблокировать этот контакт?",
"report_chat_warning": "Вы действительно хотите сообщить о нарушении и заблокировать этот чат?",
"action_clear_messages": "Удалить сообщения",
"clear_messages_warning": "Вы действительно хотите удалить все сообщения в чате? Их будет невозможно восстановить."
}, },
"zh-CN": { "zh": {
"archived_contacts": "已归档联系人 ({count})", "archived_contacts": "已归档联系人 ({count})",
"contacts_not_found": "你尚无会话<br />¯∖_(ツ)_/¯", "contacts_not_found": "你尚无会话<br />¯∖_(ツ)_/¯",
"full_name_prompt": "全名,例如张伟", "full_name_prompt": "全名,例如张伟",
@@ -315,7 +333,6 @@
"label_default_access": "默认权限模式:", "label_default_access": "默认权限模式:",
"label_group_members": "群组成员:", "label_group_members": "群组成员:",
"button_add_members": "添加成员", "button_add_members": "添加成员",
"button_leave": "离开",
"group_has_no_members": "无成员", "group_has_no_members": "无成员",
"action_leave_chat": "离开", "action_leave_chat": "离开",
"login_prompt": "登录", "login_prompt": "登录",
@@ -423,6 +440,9 @@
"label_legal": "法律东西", "label_legal": "法律东西",
"link_contact_us": "联系我们", "link_contact_us": "联系我们",
"link_terms_of_service": "条款和条件", "link_terms_of_service": "条款和条件",
"link_privacy_policy": "隐私政策" "link_privacy_policy": "隐私政策",
"action_delete_messages": "删除所有帖子",
"action_block_contact": "屏蔽联系人",
"action_report_chat": "检举垃圾邮件"
} }
} }
+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.16.1"; export const PACKAGE_VERSION = "0.16.2-rc4";
+1 -1
View File
@@ -58,7 +58,7 @@ export default class ContactsView extends React.Component {
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
if (prevProps.chatList != this.props.chatList) { if (prevProps.chatList != this.props.chatList || prevProps.archive != this.props.archive) {
const newState = ContactsView.deriveStateFromProps(this.props); const newState = ContactsView.deriveStateFromProps(this.props);
this.setState(newState); this.setState(newState);
if (newState.unreadThreads != prevState.unreadThreads) { if (newState.unreadThreads != prevState.unreadThreads) {
+3 -2
View File
@@ -9,6 +9,8 @@ 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 { vcard } from '../lib/utils.js';
import { MAX_TITLE_LENGTH } from '../config.js';
export default class CreateAccountView extends React.PureComponent { export default class CreateAccountView extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -59,12 +61,11 @@ export default class CreateAccountView extends React.PureComponent {
handleSubmit(e) { handleSubmit(e) {
e.preventDefault(); e.preventDefault();
// TODO: check email for validity
this.setState({errorCleared: false}); this.setState({errorCleared: false});
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, this.state.imageDataUrl), vcard(this.state.fn.trim().substring(0, MAX_TITLE_LENGTH), this.state.imageDataUrl),
{'meth': 'email', 'val': this.state.email}); {'meth': 'email', 'val': this.state.email});
} }
+5 -2
View File
@@ -10,7 +10,7 @@ import InPlaceEdit from '../widgets/in-place-edit.jsx';
import PermissionsEditor from '../widgets/permissions-editor.jsx'; import PermissionsEditor from '../widgets/permissions-editor.jsx';
import TagManager from '../widgets/tag-manager.jsx'; import TagManager from '../widgets/tag-manager.jsx';
import { LINK_CONTACT_US, LINK_PRIVACY_POLICY, LINK_TERMS_OF_SERVICE } from '../config.js'; import { LINK_CONTACT_US, LINK_PRIVACY_POLICY, LINK_TERMS_OF_SERVICE, MAX_TITLE_LENGTH } from '../config.js';
import { makeImageUrl } from '../lib/blob-helpers.js'; import { makeImageUrl } from '../lib/blob-helpers.js';
import { arrayEqual, asEmail, asPhone, vcard } from '../lib/utils.js'; import { arrayEqual, asEmail, asPhone, vcard } from '../lib/utils.js';
@@ -70,9 +70,12 @@ export default class EditAccountView extends React.Component {
} }
handleFullNameUpdate(fn) { handleFullNameUpdate(fn) {
fn = fn.trim().substring(0, MAX_TITLE_LENGTH);
if (fn) {
this.setState({fullName: fn}); this.setState({fullName: fn});
this.props.onUpdateAccount(undefined, vcard(fn, null)); this.props.onUpdateAccount(undefined, vcard(fn, null));
} }
}
handlePasswordUpdate(pwd) { handlePasswordUpdate(pwd) {
this.setState({password: pwd}); this.setState({password: pwd});
@@ -291,7 +294,7 @@ export default class EditAccountView extends React.Component {
<div className="hr" /> <div className="hr" />
<div className="panel-form-column"> <div className="panel-form-column">
<a href="#" className="red flat-button" onClick={(e) => {e.preventDefault(); this.props.onLogout();}}> <a href="#" className="red flat-button" onClick={(e) => {e.preventDefault(); this.props.onLogout();}}>
<i className="material-icons">exit_to_app</i> <FormattedMessage id="button_logout" <i className="material-icons">exit_to_app</i> &nbsp;<FormattedMessage id="button_logout"
defaultMessage="Logout" description="Button [Logout]" /> defaultMessage="Logout" description="Button [Logout]" />
</a> </a>
</div> </div>
+144 -22
View File
@@ -15,7 +15,7 @@ import MoreButton from '../widgets/more-button.jsx';
import PermissionsEditor from '../widgets/permissions-editor.jsx'; import PermissionsEditor from '../widgets/permissions-editor.jsx';
import TagManager from '../widgets/tag-manager.jsx'; import TagManager from '../widgets/tag-manager.jsx';
import { NO_ACCESS_MODE } from '../config.js'; import { MAX_TITLE_LENGTH, NO_ACCESS_MODE } from '../config.js';
import { makeImageUrl } from '../lib/blob-helpers.js'; import { makeImageUrl } from '../lib/blob-helpers.js';
import { arrayEqual, vcard } from '../lib/utils.js'; import { arrayEqual, vcard } from '../lib/utils.js';
@@ -40,7 +40,57 @@ const messages = defineMessages({
id: 'label_other_user', id: 'label_other_user',
defaultMessage: 'Other', defaultMessage: 'Other',
description: 'Label for the other user when the user is unnamed' description: 'Label for the other user when the user is unnamed'
} },
clear_messages: {
id: 'action_clear_messages',
defaultMessage: 'Clear Messages',
description: 'Flat button [Clear Messages] (soft-delete messages)'
},
clear_messages_warning: {
id: 'clear_messages_warning',
defaultMessage: 'Are you sure you want to clear all messages? It cannot be undone.',
description: 'Alert dialog warning when deleting all messages.'
},
delete_messages: {
id: 'action_delete_messages',
defaultMessage: 'Clear Messages for All',
description: 'Flat button [Clear for All] (hard-delete all messages)'
},
delete_messages_warning: {
id: 'delete_messages_warning',
defaultMessage: 'Are you sure you want to delete all messages for everyone? It cannot be undone.',
description: 'Alert dialog warning when hard-deleting all messages.'
},
leave_chat: {
id: 'action_leave_chat',
defaultMessage: 'Leave Conversation',
description: 'Flat button [Leave Conversation]'
},
leave_chat_warning: {
id: 'leave_chat_warning',
defaultMessage: 'Are you sure you want to leave this conversation?',
description: 'Alert dialog warning when unsubscribing from a chat.'
},
block_contact: {
id: 'action_block_contact',
defaultMessage: "Block Contact",
description: "Flat button [Block Contact]"
},
block_contact_warning: {
id: 'block_contact_warning',
defaultMessage: 'Are you sure you want to block this contact?',
description: 'Alert dialog warning when blocking a contact.'
},
report_chat: {
id: 'action_report_chat',
defaultMessage: 'Report Conversation',
description: 'Flat button [Report Group]'
},
report_chat_warning: {
id: 'report_chat_warning',
defaultMessage: 'Are you sure you want to block and report this conversation?',
description: 'Alert dialog warning when reporting a conversation for abuse'
},
}); });
class InfoView extends React.Component { class InfoView extends React.Component {
@@ -52,6 +102,7 @@ class InfoView extends React.Component {
owner: false, owner: false,
admin: false, admin: false,
sharer: false, sharer: false,
deleter: false,
muted: false, muted: false,
address: null, address: null,
groupTopic: undefined, groupTopic: undefined,
@@ -91,7 +142,10 @@ class InfoView extends React.Component {
this.handleShowAddMembers = this.handleShowAddMembers.bind(this); this.handleShowAddMembers = this.handleShowAddMembers.bind(this);
this.handleHideAddMembers = this.handleHideAddMembers.bind(this); this.handleHideAddMembers = this.handleHideAddMembers.bind(this);
this.handleMemberUpdateRequest = this.handleMemberUpdateRequest.bind(this); this.handleMemberUpdateRequest = this.handleMemberUpdateRequest.bind(this);
this.handleDeleteMessages = this.handleDeleteMessages.bind(this);
this.handleLeave = this.handleLeave.bind(this); this.handleLeave = this.handleLeave.bind(this);
this.handleBlock = this.handleBlock.bind(this);
this.handleReport = this.handleReport.bind(this);
this.handleMemberSelected = this.handleMemberSelected.bind(this); this.handleMemberSelected = this.handleMemberSelected.bind(this);
this.handleMoreInfo = this.handleMoreInfo.bind(this); this.handleMoreInfo = this.handleMoreInfo.bind(this);
this.handleTagsUpdated = this.handleTagsUpdated.bind(this); this.handleTagsUpdated = this.handleTagsUpdated.bind(this);
@@ -168,6 +222,7 @@ class InfoView extends React.Component {
owner: acs && acs.isOwner(), owner: acs && acs.isOwner(),
admin: acs && acs.isAdmin(), admin: acs && acs.isAdmin(),
sharer: acs && acs.isSharer(), sharer: acs && acs.isSharer(),
deleter: acs && acs.isDeleter(),
muted: acs && acs.isMuted(), muted: acs && acs.isMuted(),
fullName: topic.public ? topic.public.fn : undefined, fullName: topic.public ? topic.public.fn : undefined,
@@ -222,17 +277,18 @@ class InfoView extends React.Component {
} }
handleFullNameUpdate(fn) { handleFullNameUpdate(fn) {
fn = fn.trim(); 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, vcard(fn, null), null);
} }
} }
handlePrivateUpdate(priv) { handlePrivateUpdate(comment) {
if (this.state.priv !== priv) { comment = comment.trim().substring(0, MAX_TITLE_LENGTH);
this.setState({private: priv}); if (this.state.private !== comment) {
this.props.onTopicDescUpdate(this.props.topic, null, priv || Tinode.DEL_CHAR); this.setState({private: comment});
this.props.onTopicDescUpdate(this.props.topic, null, comment || Tinode.DEL_CHAR);
} }
} }
@@ -354,9 +410,56 @@ class InfoView extends React.Component {
this.setState({showMemberPanel: false}); this.setState({showMemberPanel: false});
} }
handleDeleteMessages(e) {
e.preventDefault();
const {formatMessage} = this.props.intl;
this.props.onShowAlert(
formatMessage(this.state.deleter ? messages.delete_messages : messages.clear_messages), // title
formatMessage(this.state.deleter ? messages.delete_messages_warning : messages.clear_messages_warning), // content
(() => { this.props.onDeleteMessages(this.props.topic); }), // onConfirm
null, // "OK"
true, // Show Reject button
null // "Cancel"
);
}
handleLeave(e) { handleLeave(e) {
e.preventDefault(); e.preventDefault();
this.props.onLeaveTopic(this.props.topic); const {formatMessage} = this.props.intl;
this.props.onShowAlert(
formatMessage(messages.leave_chat), // title
formatMessage(messages.leave_chat_warning), // content
(() => { this.props.onLeaveTopic(this.props.topic); }), // onConfirm
null, // "OK"
true, // Show Reject button
null // "Cancel"
);
}
handleBlock(e) {
e.preventDefault();
const {formatMessage} = this.props.intl;
this.props.onShowAlert(
formatMessage(messages.block_contact), // title
formatMessage(messages.block_contact_warning), // content
(() => { this.props.onBlockTopic(this.props.topic); }), // onConfirm
null, // "OK"
true, // Show Reject button
null // "Cancel"
);
}
handleReport(e) {
e.preventDefault();
const {formatMessage} = this.props.intl;
this.props.onShowAlert(
formatMessage(messages.report_chat), // title
formatMessage(messages.report_chat_warning), // content
(() => { this.props.onReportTopic(this.props.topic); }), // onConfirm
null, // "OK"
true, // Show Reject button
null // "Cancel"
);
} }
handleMemberSelected(uid) { handleMemberSelected(uid) {
@@ -422,6 +525,7 @@ class InfoView extends React.Component {
<GroupManager <GroupManager
members={this.state.contactList} members={this.state.contactList}
requiredMember={this.props.myUserId} requiredMember={this.props.myUserId}
keepInitialMembers={!this.state.admin && !this.state.owner}
myUserId={this.props.myUserId} myUserId={this.props.myUserId}
contacts={this.props.searchableContacts} contacts={this.props.searchableContacts}
onCancel={this.handleHideAddMembers} onCancel={this.handleHideAddMembers}
@@ -575,6 +679,7 @@ class InfoView extends React.Component {
</div> </div>
<div className="hr" /> <div className="hr" />
{this.state.owner ? {this.state.owner ?
<>
<FormattedMessage id="title_tag_manager">{ <FormattedMessage id="title_tag_manager">{
(tags) => <TagManager (tags) => <TagManager
title={tags} title={tags}
@@ -582,11 +687,38 @@ class InfoView extends React.Component {
activated={false} activated={false}
onSubmit={this.handleTagsUpdated} /> onSubmit={this.handleTagsUpdated} />
}</FormattedMessage> }</FormattedMessage>
<div className="hr" />
</>
: :
null null
} }
{this.state.owner ? <div className="hr" /> : null } <div className="panel-form-column">
<a href="#" className="flat-button" onClick={this.handleDeleteMessages}>
<i className="material-icons">delete_outline</i> &nbsp;{
formatMessage(this.state.deleter ? messages.delete_messages : messages.clear_messages)
}
</a>
<a href="#" className="red flat-button" onClick={this.handleLeave}>
<i className="material-icons">exit_to_app</i> &nbsp;{formatMessage(messages.leave_chat)}
</a>
{!this.state.groupTopic ?
<a href="#" className="red flat-button" onClick={this.handleBlock}>
<i className="material-icons">block</i> &nbsp;{formatMessage(messages.block_contact)}
</a>
:
null
}
{!this.state.owner ?
<a href="#" className="red flat-button" onClick={this.handleReport}>
<i className="material-icons">report</i> &nbsp;{formatMessage(messages.report_chat)}
</a>
:
null
}
</div>
{this.state.groupTopic ? {this.state.groupTopic ?
<>
<div className="hr" />
<div className="panel-form-column"> <div className="panel-form-column">
<div className="panel-form-row"> <div className="panel-form-row">
<label className="small"> <label className="small">
@@ -597,16 +729,10 @@ class InfoView extends React.Component {
<div className="panel-form-row"> <div className="panel-form-row">
{this.state.sharer ? {this.state.sharer ?
<a href="#" className="flat-button" onClick={this.handleShowAddMembers}> <a href="#" className="flat-button" onClick={this.handleShowAddMembers}>
<i className="material-icons">person_add</i> <FormattedMessage id="button_add_members" <i className="material-icons">person_add</i> &nbsp;<FormattedMessage id="button_add_members"
defaultMessage="Add members" description="Flat button [Add members] (to topic)" /> defaultMessage="Add members" description="Flat button [Add members] (to topic)" />
</a> </a>
: null} : null}
{!this.state.owner ?
<a href="#" className="red flat-button" onClick={this.handleLeave}>
<i className="material-icons">exit_to_app</i> <FormattedMessage id="button_leave"
defaultMessage="Leave" description="Flat button [Leave] (topic)" />
</a>
: null}
</div> </div>
<FormattedMessage id="group_has_no_members" defaultMessage="No members" <FormattedMessage id="group_has_no_members" defaultMessage="No members"
description="Shown in place of group members">{ description="Shown in place of group members">{
@@ -624,13 +750,9 @@ class InfoView extends React.Component {
/> />
}</FormattedMessage> }</FormattedMessage>
</div> </div>
</>
: :
<div className="panel-form-row"> null
<a href="#" className="red flat-button" onClick={this.handleLeave}>
<i className="material-icons">exit_to_app</i> <FormattedMessage id="action_leave_chat"
defaultMessage="Leave" description="Action [Leave] chat" />
</a>
</div>
} }
</div> </div>
} }
+5 -7
View File
@@ -14,7 +14,7 @@ import LoadSpinner from '../widgets/load-spinner.jsx';
import LogoView from './logo-view.jsx'; import LogoView from './logo-view.jsx';
import SendMessage from '../widgets/send-message.jsx'; import SendMessage from '../widgets/send-message.jsx';
import { DEFAULT_ACCESS_MODE, KEYPRESS_DELAY, MESSAGES_PAGE } from '../config.js'; import { DEFAULT_P2P_ACCESS_MODE, KEYPRESS_DELAY, MESSAGES_PAGE } from '../config.js';
import { makeImageUrl } from '../lib/blob-helpers.js'; import { makeImageUrl } from '../lib/blob-helpers.js';
import { shortDateFormat } from '../lib/strformat.js'; import { shortDateFormat } from '../lib/strformat.js';
@@ -85,7 +85,6 @@ class MessagesView extends React.Component {
} }
componentWillUnmount() { componentWillUnmount() {
this.leave(this.state.topic);
if (this.messagesScroller) { if (this.messagesScroller) {
this.messagesScroller.removeEventListener('scroll', this.handleScrollEvent); this.messagesScroller.removeEventListener('scroll', this.handleScrollEvent);
} }
@@ -104,7 +103,7 @@ class MessagesView extends React.Component {
const topic = this.props.tinode.getTopic(this.state.topic); const topic = this.props.tinode.getTopic(this.state.topic);
if (this.state.topic != prevState.topic) { if (this.state.topic != prevState.topic) {
if (prevState.topic) { if (prevState.topic && !Tinode.isNewGroupTopicName(prevState.topic)) {
this.leave(prevState.topic); this.leave(prevState.topic);
this.props.readTimerHandler(null); this.props.readTimerHandler(null);
} }
@@ -379,9 +378,9 @@ class MessagesView extends React.Component {
// Regenerate messages list // Regenerate messages list
const topic = this.props.tinode.getTopic(this.state.topic); const topic = this.props.tinode.getTopic(this.state.topic);
const newState = {messages: []}; const newState = {messages: []};
topic.messages(function(m) { topic.messages((m) => {
if (!m.deleted) { if (!m.deleted) {
newState.messages = newState.messages.concat(m); newState.messages.push(m);
} }
}); });
@@ -403,7 +402,6 @@ class MessagesView extends React.Component {
} }
this.props.onData(msg); this.props.onData(msg);
} }
this.setState(newState); this.setState(newState);
} }
@@ -491,7 +489,7 @@ class MessagesView extends React.Component {
handleEnablePeer(e) { handleEnablePeer(e) {
e.preventDefault(); e.preventDefault();
this.props.onChangePermissions(this.state.topic, DEFAULT_ACCESS_MODE, this.state.topic); this.props.onChangePermissions(this.state.topic, DEFAULT_P2P_ACCESS_MODE, this.state.topic);
} }
render() { render() {
+4 -1
View File
@@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import SideNavbar from '../widgets/side-navbar.jsx';
import ErrorPanel from '../widgets/error-panel.jsx'; import ErrorPanel from '../widgets/error-panel.jsx';
import LoadSpinner from '../widgets/load-spinner.jsx';
import SideNavbar from '../widgets/side-navbar.jsx';
import ContactsView from './contacts-view.jsx'; import ContactsView from './contacts-view.jsx';
import CreateAccountView from './create-account-view.jsx'; import CreateAccountView from './create-account-view.jsx';
@@ -105,6 +106,8 @@ class SidepanelView extends React.Component {
actionText={this.props.errorActionText} actionText={this.props.errorActionText}
onClearError={this.props.onError} /> onClearError={this.props.onError} />
<LoadSpinner show={this.props.loadSpinnerVisible} />
{view === 'login' ? {view === 'login' ?
<LoginView <LoginView
login={this.props.login} login={this.props.login}
+110 -15
View File
@@ -8,13 +8,15 @@ import 'firebase/messaging';
import Tinode from 'tinode-sdk'; import Tinode from 'tinode-sdk';
import Alert from '../widgets/alert.jsx';
import ContextMenu from '../widgets/context-menu.jsx'; import ContextMenu from '../widgets/context-menu.jsx';
import InfoView from './info-view.jsx'; import InfoView from './info-view.jsx';
import MessagesView from './messages-view.jsx'; import MessagesView from './messages-view.jsx';
import SidepanelView from './sidepanel-view.jsx'; import SidepanelView from './sidepanel-view.jsx';
import { API_KEY, APP_NAME, DEFAULT_ACCESS_MODE, MEDIA_BREAKPOINT, READ_DELAY, RECEIVED_DELAY } from '../config.js'; import { API_KEY, APP_NAME, DEFAULT_P2P_ACCESS_MODE, LOGGING_ENABLED, MEDIA_BREAKPOINT,
READ_DELAY, RECEIVED_DELAY } from '../config.js';
import { base64ReEncode, makeImageUrl } from '../lib/blob-helpers.js'; import { base64ReEncode, makeImageUrl } 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';
@@ -98,11 +100,14 @@ class TinodeWeb extends React.Component {
this.handleChangePermissions = this.handleChangePermissions.bind(this); this.handleChangePermissions = this.handleChangePermissions.bind(this);
this.handleTagsUpdated = this.handleTagsUpdated.bind(this); this.handleTagsUpdated = this.handleTagsUpdated.bind(this);
this.handleLogout = this.handleLogout.bind(this); this.handleLogout = this.handleLogout.bind(this);
this.handleDeleteMessagesRequest = this.handleDeleteMessagesRequest.bind(this);
this.handleLeaveUnsubRequest = this.handleLeaveUnsubRequest.bind(this); this.handleLeaveUnsubRequest = this.handleLeaveUnsubRequest.bind(this);
this.handleDialogCancel = this.handleDialogCancel.bind(this); this.handleBlockTopicRequest = this.handleBlockTopicRequest.bind(this);
this.handleReportTopic = this.handleReportTopic.bind(this);
this.handleShowContextMenu = this.handleShowContextMenu.bind(this); this.handleShowContextMenu = this.handleShowContextMenu.bind(this);
this.defaultTopicContextMenu = this.defaultTopicContextMenu.bind(this); this.defaultTopicContextMenu = this.defaultTopicContextMenu.bind(this);
this.handleHideContextMenu = this.handleHideContextMenu.bind(this); this.handleHideContextMenu = this.handleHideContextMenu.bind(this);
this.handleShowAlert = this.handleShowAlert.bind(this);
this.handleShowInfoView = this.handleShowInfoView.bind(this); this.handleShowInfoView = this.handleShowInfoView.bind(this);
this.handleHideInfoView = this.handleHideInfoView.bind(this); this.handleHideInfoView = this.handleHideInfoView.bind(this);
this.handleMemberUpdateRequest = this.handleMemberUpdateRequest.bind(this); this.handleMemberUpdateRequest = this.handleMemberUpdateRequest.bind(this);
@@ -138,8 +143,8 @@ class TinodeWeb extends React.Component {
sidePanelSelected: 'login', sidePanelSelected: 'login',
sidePanelTitle: null, sidePanelTitle: null,
sidePanelAvatar: null, sidePanelAvatar: null,
dialogSelected: null, loadSpinnerVisible: false,
contextMenuVisible: false,
login: '', login: '',
password: '', password: '',
myUserId: null, myUserId: null,
@@ -159,6 +164,10 @@ class TinodeWeb extends React.Component {
contextMenuParams: null, contextMenuParams: null,
contextMenuItems: [], contextMenuItems: [],
// Modal alert dialog.
alertVisible: false,
alertParams: {},
// Chats as shown in the ContactsView // Chats as shown in the ContactsView
chatList: [], chatList: [],
// Contacts returned by a search query. // Contacts returned by a search query.
@@ -250,7 +259,7 @@ class TinodeWeb extends React.Component {
// Setup transport (usually websocket) and server address. This will terminate connection with the server. // Setup transport (usually websocket) and server address. This will terminate connection with the server.
static tnSetup(serverAddress, transport) { static tnSetup(serverAddress, transport) {
let tinode = new Tinode(APP_NAME, serverAddress, API_KEY, transport, isSecureConnection()); let tinode = new Tinode(APP_NAME, serverAddress, API_KEY, transport, isSecureConnection());
tinode.enableLogging(true, true); tinode.enableLogging(LOGGING_ENABLED, true);
return tinode; return tinode;
} }
@@ -351,7 +360,12 @@ class TinodeWeb extends React.Component {
// User clicked Login button in the side panel. // User clicked Login button in the side panel.
handleLoginRequest(login, password) { handleLoginRequest(login, password) {
this.setState({loginDisabled: true, login: login, password: password}); this.setState({
loginDisabled: true,
login: login,
password: password,
loadSpinnerVisible: true
});
this.handleError('', null); this.handleError('', null);
if (this.tinode.isConnected()) { if (this.tinode.isConnected()) {
@@ -419,7 +433,6 @@ class TinodeWeb extends React.Component {
connected: false, connected: false,
ready: false, ready: false,
topicSelectedOnline: false, topicSelectedOnline: false,
dialogSelected: null,
errorText: err && err.message ? err.message : "Disconnected", errorText: err && err.message ? err.message : "Disconnected",
errorLevel: err && err.message ? 'err' : 'warn', errorLevel: err && err.message ? 'err' : 'warn',
loginDisabled: false, loginDisabled: false,
@@ -449,6 +462,7 @@ class TinodeWeb extends React.Component {
if (promise) { if (promise) {
promise.then((ctrl) => { promise.then((ctrl) => {
if (ctrl.code >= 300 && ctrl.text === 'validate credentials') { if (ctrl.code >= 300 && ctrl.text === 'validate credentials') {
this.setState({loadSpinnerVisible: false});
if (cred) { if (cred) {
this.handleError("Code does not match", 'warn'); this.handleError("Code does not match", 'warn');
} }
@@ -458,7 +472,12 @@ class TinodeWeb extends React.Component {
} }
}).catch((err) => { }).catch((err) => {
// Login failed, report error. // Login failed, report error.
this.setState({loginDisabled: false, credMethod: undefined, credCode: undefined}); this.setState({
loginDisabled: false,
credMethod: undefined,
credCode: undefined,
loadSpinnerVisible: false
});
this.handleError(err.message, 'err'); this.handleError(err.message, 'err');
localStorage.removeItem('auth-token'); localStorage.removeItem('auth-token');
HashNavigation.navigateTo(''); HashNavigation.navigateTo('');
@@ -505,9 +524,12 @@ class TinodeWeb extends React.Component {
withCred(). withCred().
build() build()
).catch((err) => { ).catch((err) => {
this.tinode.disconnect();
localStorage.removeItem('auth-token'); localStorage.removeItem('auth-token');
this.handleError(err.message, 'err'); this.handleError(err.message, 'err');
HashNavigation.navigateTo(''); HashNavigation.navigateTo('');
}).finally(() => {
this.setState({loadSpinnerVisible: false});
}); });
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, 'contacts')); HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, 'contacts'));
} }
@@ -566,11 +588,12 @@ class TinodeWeb extends React.Component {
this.setState({topicSelectedAcs: cont.acs}); this.setState({topicSelectedAcs: cont.acs});
} }
} else if (what == 'del') { } else if (what == 'del') {
// messages deleted (hard or soft) -- update pill counter. // TODO: messages deleted (hard or soft) -- update pill counter.
} else if (what == 'upd') {
// upd - handled by the SDK. Explicitly ignoring here.
} else { } else {
// TODO(gene): handle other types of notifications: // TODO(gene): handle other types of notifications:
// * ua -- user agent changes (maybe display a pictogram for mobile/desktop). // * ua -- user agent changes (maybe display a pictogram for mobile/desktop).
// * upd -- topic 'public' updated, issue getMeta().
console.log("Unsupported (yet) presence update:" + what + " in: " + cont.topic); console.log("Unsupported (yet) presence update:" + what + " in: " + cont.topic);
} }
} }
@@ -984,15 +1007,16 @@ class TinodeWeb extends React.Component {
// Request to start a new topic. New P2P topic requires peer's name. // Request to start a new topic. New P2P topic requires peer's name.
handleNewTopicRequest(peerName, pub, priv, tags) { handleNewTopicRequest(peerName, pub, priv, tags) {
const topicName = peerName || this.tinode.newGroupTopicName(); const topicName = peerName || this.tinode.newGroupTopicName();
const params = { const params = {
_topicName: topicName, _topicName: topicName,
}; };
if (peerName) { if (peerName) {
// Because we are initialing the subscription, set 'want' to all permissions. // Because we are initialing the subscription, set 'want' to all permissions.
params.sub = {mode: DEFAULT_ACCESS_MODE}; params.sub = {mode: DEFAULT_P2P_ACCESS_MODE};
// Give the other user all permissions too. // Give the other user all permissions too.
params.desc = {defacs: {auth: DEFAULT_ACCESS_MODE}}; params.desc = {defacs: {auth: DEFAULT_P2P_ACCESS_MODE}};
} else { } else {
params.desc = {public: pub, private: {comment: priv}}; params.desc = {public: pub, private: {comment: priv}};
params.tags = tags; params.tags = tags;
@@ -1080,6 +1104,18 @@ class TinodeWeb extends React.Component {
HashNavigation.navigateTo(''); HashNavigation.navigateTo('');
} }
handleDeleteMessagesRequest(topicName) {
const topic = this.tinode.getTopic(topicName);
if (!topic) {
return;
}
// Request hard-delete all messages.
topic.delMessagesAll(true).catch((err) => {
this.handleError(err.message, 'err');
});
}
handleLeaveUnsubRequest(topicName) { handleLeaveUnsubRequest(topicName) {
const topic = this.tinode.getTopic(topicName); const topic = this.tinode.getTopic(topicName);
if (!topic) { if (!topic) {
@@ -1094,8 +1130,39 @@ class TinodeWeb extends React.Component {
}); });
} }
handleDialogCancel() { handleBlockTopicRequest(topicName) {
this.setState({dialogSelected: null}); const topic = this.tinode.getTopic(topicName);
if (!topic) {
return;
}
topic.updateMode(null, '-JP').then((ctrl) => {
// Hide MessagesView and InfoView panels.
HashNavigation.navigateTo(HashNavigation.setUrlTopic(window.location.hash, ''));
}).catch((err) => {
this.handleError(err.message, 'err');
});
}
handleReportTopic(topicName) {
const topic = this.tinode.getTopic(topicName);
if (!topic) {
return;
}
// Publish spam report.
this.tinode.publish(Tinode.TOPIC_SYS, Tinode.Drafty.attachJSON(null, {
'action': 'report',
'target': topicName
}));
// Remove J and P permissions.
topic.updateMode(null, '-JP').then((ctrl) => {
// Hide MessagesView and InfoView panels.
HashNavigation.navigateTo(HashNavigation.setUrlTopic(window.location.hash, ''));
}).catch((err) => {
this.handleError(err.message, 'err');
});
} }
handleShowContextMenu(params, menuItems) { handleShowContextMenu(params, menuItems) {
@@ -1156,6 +1223,20 @@ class TinodeWeb extends React.Component {
} }
} }
handleShowAlert(title, content, onConfirm, confirm, onReject, reject) {
this.setState({
alertVisible: true,
alertParams: {
title: title,
content: content,
onConfirm: onConfirm,
confirm: confirm,
onReject: onReject,
reject: reject
}
});
}
handleShowInfoView() { handleShowInfoView() {
HashNavigation.navigateTo(HashNavigation.addUrlParam(window.location.hash, 'info', true)); HashNavigation.navigateTo(HashNavigation.addUrlParam(window.location.hash, 'info', true));
this.setState({showInfoPanel: true}); this.setState({showInfoPanel: true});
@@ -1248,6 +1329,7 @@ class TinodeWeb extends React.Component {
params={this.state.contextMenuParams} params={this.state.contextMenuParams}
items={this.state.contextMenuItems} items={this.state.contextMenuItems}
hide={this.handleHideContextMenu} hide={this.handleHideContextMenu}
onShowAlert={this.handleShowAlert}
onAction={this.handleContextMenuAction} onAction={this.handleContextMenuAction}
onTopicRemoved={(topicName) => { onTopicRemoved={(topicName) => {
if (topicName == this.state.topicSelected) { if (topicName == this.state.topicSelected) {
@@ -1258,7 +1340,15 @@ class TinodeWeb extends React.Component {
: :
null null
} }
<Alert
visible={this.state.alertVisible}
title={this.state.alertParams.title}
content={this.state.alertParams.content}
onReject={this.state.alertParams.onReject ? (() => { this.setState({alertVisible: false}); }) : null}
reject={this.state.alertParams.reject}
onConfirm={() => { this.setState({alertVisible: false}); this.state.alertParams.onConfirm(); }}
confirm={this.state.alertParams.confirm}
/>
<SidepanelView <SidepanelView
tinode={this.tinode} tinode={this.tinode}
connected={this.state.connected} connected={this.state.connected}
@@ -1270,6 +1360,7 @@ class TinodeWeb extends React.Component {
login={this.state.login} login={this.state.login}
myUserId={this.state.myUserId} myUserId={this.state.myUserId}
loginDisabled={this.state.loginDisabled} loginDisabled={this.state.loginDisabled}
loadSpinnerVisible={this.state.loadSpinnerVisible}
errorText={this.state.errorText} errorText={this.state.errorText}
errorLevel={this.state.errorLevel} errorLevel={this.state.errorLevel}
@@ -1365,9 +1456,13 @@ class TinodeWeb extends React.Component {
onTopicDescUpdate={this.handleTopicUpdateRequest} onTopicDescUpdate={this.handleTopicUpdateRequest}
onCancel={this.handleHideInfoView} onCancel={this.handleHideInfoView}
onShowAlert={this.handleShowAlert}
onChangePermissions={this.handleChangePermissions} onChangePermissions={this.handleChangePermissions}
onMemberUpdateRequest={this.handleMemberUpdateRequest} onMemberUpdateRequest={this.handleMemberUpdateRequest}
onDeleteMessages={this.handleDeleteMessagesRequest}
onLeaveTopic={this.handleLeaveUnsubRequest} onLeaveTopic={this.handleLeaveUnsubRequest}
onBlockTopic={this.handleBlockTopicRequest}
onReportTopic={this.handleReportTopic}
onAddMember={this.handleManageGroupMembers} onAddMember={this.handleManageGroupMembers}
onTopicTagsUpdate={this.handleTagsUpdated} onTopicTagsUpdate={this.handleTagsUpdated}
onInitFind={this.tnInitFind} onInitFind={this.tnInitFind}
+28
View File
@@ -0,0 +1,28 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
export default class Alert extends React.PureComponent {
render() {
return this.props.visible ?
<div className="alert-container">
<div className="alert">
<div className="title">{this.props.title}</div>
<div className="content">{this.props.content}</div>
<div className="dialog-buttons">
{this.props.onReject ?
<button className="outline" onClick={this.props.onReject}>
{this.props.reject || <FormattedMessage id="button_cancel" />}
</button>
:
null
}
<button className="blue" onClick={this.props.onConfirm}>
{this.props.confirm || <FormattedMessage id="button_ok" />}
</button>
</div>
</div>
</div>
:
null;
}
};
+9 -8
View File
@@ -24,14 +24,14 @@ export default class ChipInput extends React.Component {
static deriveStateFromProps(props) { static deriveStateFromProps(props) {
return { return {
placeholder: props.chips ? '' : props.prompt, placeholder: props.chips ? '' : props.prompt,
sortedChips: ChipInput.sortChips(props.chips, props.required), sortedChips: ChipInput.sortChips(props.chips, props.staticMembers),
chipIndex: ChipInput.indexChips(props.chips) chipIndex: ChipInput.indexChips(props.chips)
}; };
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
if (prevProps.chips != this.props.chips || if (prevProps.chips != this.props.chips ||
prevProps.required != this.props.required || prevProps.staticMembers != this.props.staticMembers ||
prevProps.prompt != this.props.prompt) { prevProps.prompt != this.props.prompt) {
this.setState(ChipInput.deriveStateFromProps(this.props)); this.setState(ChipInput.deriveStateFromProps(this.props));
} }
@@ -44,7 +44,7 @@ export default class ChipInput extends React.Component {
static indexChips(chips) { static indexChips(chips) {
const index = {}; const index = {};
let count = 0; let count = 0;
chips.map(function(item) { chips.map((item) => {
index[item.user] = count; index[item.user] = count;
count ++; count ++;
}); });
@@ -55,8 +55,8 @@ export default class ChipInput extends React.Component {
static sortChips(chips, keep) { static sortChips(chips, keep) {
const required = []; const required = [];
const normal = []; const normal = [];
chips.map(function(item) { chips.map((item) => {
if (item.user === keep) { if (keep && keep.includes(item.user)) {
required.push(item); required.push(item);
} else { } else {
normal.push(item); normal.push(item);
@@ -95,8 +95,8 @@ export default class ChipInput extends React.Component {
handleKeyDown(e) { handleKeyDown(e) {
if (e.key === 'Backspace') { if (e.key === 'Backspace') {
if (this.state.input.length == 0 && this.state.sortedChips.length > 0) { if (this.state.input.length == 0 && this.state.sortedChips.length > 0) {
var at = this.state.sortedChips.length - 1; const at = this.state.sortedChips.length - 1;
if (this.state.sortedChips[at].user !== this.props.required) { if (this.state.sortedChips[at].user !== this.props.staticMembers) {
this.removeChipAt(at); this.removeChipAt(at);
} }
} }
@@ -114,6 +114,7 @@ export default class ChipInput extends React.Component {
render() { render() {
const chips = []; const chips = [];
let count = 0; let count = 0;
const staticMembers = this.props.staticMembers || [];
this.state.sortedChips.map((item) => { this.state.sortedChips.map((item) => {
chips.push( chips.push(
<Chip <Chip
@@ -122,7 +123,7 @@ export default class ChipInput extends React.Component {
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}
required={item.user === this.props.required} required={staticMembers.includes(item.user)}
invalid={item.invalid} invalid={item.invalid}
index={count} index={count}
key={item.user} /> key={item.user} />
+1 -1
View File
@@ -70,7 +70,7 @@ export default class Contact extends React.Component {
<UnreadBadge count={this.props.unread} /><ContactBadges badges={icon_badges} /> <UnreadBadge count={this.props.unread} /><ContactBadges badges={icon_badges} />
</div> </div>
{this.props.comment ? <div className="contact-comment">{this.props.comment}</div> : null} {this.props.comment ? <div className="contact-comment">{this.props.comment}</div> : null}
<><ContactBadges badges={badges} /></> <span><ContactBadges badges={badges} /></span>
</div> </div>
{this.props.showContextMenu ? {this.props.showContextMenu ?
<span className="menuTrigger"> <span className="menuTrigger">
+49 -18
View File
@@ -47,15 +47,26 @@ const messages = defineMessages({
defaultMessage: 'Delete', defaultMessage: 'Delete',
description: 'Delete entire topic' description: 'Delete entire topic'
}, },
topic_delete_warning: {
id: 'topic_delete_warning',
defaultMessage: 'Are you sure you want to delete this conversation?',
description: 'Alert warning when deleting entire topic'
},
unblock: { unblock: {
id: 'menu_item_unblock', id: 'menu_item_unblock',
defaultMessage: 'Unblock', defaultMessage: 'Unblock',
description: 'Unblock user' description: 'Unblock topic or user'
}, },
// Generic string suitable for either topic or user.
block: { block: {
id: 'menu_item_block', id: 'menu_item_block',
defaultMessage: 'Block', defaultMessage: 'Block',
description: 'Block user' description: 'Block topic or user'
},
topic_block_warning: {
id: 'topic_block_warning',
defaultMessage: 'Are you sure you want to block this conversation?',
description: 'Alert warning when blocking a topic.'
}, },
member_delete: { member_delete: {
id: 'menu_item_member_delete', id: 'menu_item_member_delete',
@@ -90,14 +101,28 @@ class ContextMenu extends React.Component {
id: 'messages_clear', id: 'messages_clear',
title: formatMessage(messages.clear_messages), title: formatMessage(messages.clear_messages),
handler: (params, errorHandler) => { handler: (params, errorHandler) => {
return this.deleteMessages(true, false, params, errorHandler); return props.onShowAlert(
formatMessage({id: 'menu_item_clear_messages'}), // title
formatMessage({id: 'clear_messages_warning'}), // content
(() => { this.deleteMessages(true, false, params, errorHandler); }), // onConfirm
null, // "OK"
true, // Show Reject button
null // "Cancel"
);
} }
}, },
'messages_clear_hard': { 'messages_clear_hard': {
id: 'messages_clear_hard', id: 'messages_clear_hard',
title: formatMessage(messages.clear_for_all), title: formatMessage(messages.clear_for_all),
handler: (params, errorHandler) => { handler: (params, errorHandler) => {
return this.deleteMessages(true, true, params, errorHandler); return props.onShowAlert(
formatMessage({id: 'menu_item_clear_messages_for_all'}), // title
formatMessage({id: 'delete_messages_warning'}), // content
(() => { return this.deleteMessages(true, true, params, errorHandler); }),
null, // "OK"
true, // Show Reject button
null // "Cancel"
);
} }
}, },
'message_delete': { 'message_delete': {
@@ -133,16 +158,29 @@ class ContextMenu extends React.Component {
id: 'topic_block', id: 'topic_block',
title: formatMessage(messages.block), title: formatMessage(messages.block),
handler: (params, errorHandler) => { handler: (params, errorHandler) => {
return props.onShowAlert(
formatMessage({id: 'menu_item_block'}), // title
formatMessage(messages.topic_block_warning), // content
(() => {
return this.topicPermissionSetter('-JP', params, errorHandler).then((ctrl) => { return this.topicPermissionSetter('-JP', params, errorHandler).then((ctrl) => {
this.props.onTopicRemoved(params.topicName); this.props.onTopicRemoved(params.topicName);
return ctrl; return ctrl;
}); });
}),
null, // "OK"
true, // Show Reject button
null // "Cancel"
);
} }
}, },
'topic_delete': { 'topic_delete': {
id: 'topic_delete', id: 'topic_delete',
title: formatMessage(messages.topic_delete), title: formatMessage(messages.topic_delete),
handler: (params, errorHandler) => { handler: (params, errorHandler) => {
return props.onShowAlert(
formatMessage({id: 'menu_item_delete_topic'}), // title
formatMessage(messages.topic_delete_warning), // content
(() => {
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.log("Topic not found: ", params.topicName);
@@ -153,6 +191,11 @@ class ContextMenu extends React.Component {
errorHandler(err.message, 'err'); errorHandler(err.message, 'err');
} }
}); });
}),
null, // "OK"
true, // Show Reject button
null // "Cancel"
);
} }
}, },
'topic_archive': { 'topic_archive': {
@@ -277,7 +320,7 @@ class ContextMenu extends React.Component {
return; return;
} }
// Can't cancel. Delete instead. // Can't cancel. Delete instead.
let promise = all ? const promise = all ?
topic.delMessagesAll(hard) : topic.delMessagesAll(hard) :
topic.delMessagesList([params.seq], hard); topic.delMessagesList([params.seq], hard);
@@ -296,19 +339,7 @@ class ContextMenu extends React.Component {
return; return;
} }
let am, user; let result = topic.updateMode(params.user, mode);
if (params.user) {
user = topic.subscriber(params.user);
if (!user) {
console.log("Subscriber not found", params.topicName + "[" + params.user + "]");
return;
}
am = user.acs.updateGiven(mode).getGiven();
} else {
am = topic.getAccessMode().updateWant(mode).getWant();
}
let result = topic.setMeta({sub: {user: params.user, mode: am}});
if (errorHandler) { if (errorHandler) {
result = result.catch((err) => { result = result.catch((err) => {
errorHandler(err.message, 'err'); errorHandler(err.message, 'err');
+8 -4
View File
@@ -31,18 +31,22 @@ export default class ErrorPanel extends React.PureComponent {
render() { render() {
const icons = {err: 'error', warn: 'warning', info: 'info'} const icons = {err: 'error', warn: 'warning', info: 'info'}
const level = icons[this.props.level] || ''; const level = icons[this.props.level] || '';
const className = 'alert-box ' + level; const className = 'info-box ' + level;
return ( return (
<div className={className}> <div className={className}>
<div className="icon"><i className="material-icons">{level}</i></div> <div className="icon"><i className="material-icons">{level}</i></div>
<> <span>
<span dangerouslySetInnerHTML={{__html: this.props.text}} /> <span dangerouslySetInnerHTML={{__html: this.props.text}} />
{this.props.action ? {this.props.action ?
<a href="#" onClick={(e) => {e.preventDefault(); this.props.action();}}> <>
&#32;<a href="#"
style={{ whiteSpace: 'nowrap' }}
onClick={(e) => {e.preventDefault(); this.props.action();}}>
{this.props.actionText} {this.props.actionText}
</a> </a>
: null}
</> </>
: null}
</span>
<div className="cancel"><MenuCancel onCancel={this.hide} /></div> <div className="cancel"><MenuCancel onCancel={this.hide} /></div>
</div> </div>
); );
+23 -11
View File
@@ -23,8 +23,10 @@ class GroupManager extends React.Component {
super(props); super(props);
this.state = { this.state = {
// Array of topic members
members: props.members, members: props.members,
index: GroupManager.indexMembers(props.members), index: GroupManager.indexMembers(props.members),
staticMembers: GroupManager.staticMembers(props.members, props.keepInitialMembers, props.requiredMember),
contactFilter: '', contactFilter: '',
noContactsMessage: props.intl.formatMessage(messages.no_contacts), noContactsMessage: props.intl.formatMessage(messages.no_contacts),
selectedContacts: GroupManager.selectedContacts(props.members) selectedContacts: GroupManager.selectedContacts(props.members)
@@ -39,15 +41,25 @@ class GroupManager extends React.Component {
static indexMembers(members) { static indexMembers(members) {
let index = {}; let index = {};
members.map(function(m) { members.map((m) => {
index[m.user] = {delta: 0, present: true}; // Delta: 0 unchanged, +1 added, -1 removed index[m.user] = {delta: 0, present: true}; // Delta: 0 unchanged, +1 added, -1 removed
}); });
return index; return index;
} }
static staticMembers(members, keepInitial, requiredMember) {
let stat = [];
members.map((m) => {
if (keepInitial || m.user == requiredMember) {
stat.push(m.user);
}
});
return stat;
}
static selectedContacts(members) { static selectedContacts(members) {
let sel = []; let sel = [];
members.map(function(m) { members.map((m) => {
sel.push(m.user); sel.push(m.user);
}); });
return sel; return sel;
@@ -66,31 +78,31 @@ class GroupManager extends React.Component {
status = {delta: 1, present: true}; status = {delta: 1, present: true};
} }
var m = this.state.members.slice(); let m = this.state.members.slice();
m.push(this.props.contacts[index]); m.push(this.props.contacts[index]);
var sel = GroupManager.selectedContacts(m); const sel = GroupManager.selectedContacts(m);
var i = this.state.index; const i = this.state.index;
i[userId] = status; i[userId] = status;
this.setState({members: m, index: i, selectedContacts: sel}); this.setState({members: m, index: i, selectedContacts: sel});
} }
handleMemberRemoved(userId, index) { handleMemberRemoved(userId, index) {
var status = this.state.index[userId]; const status = this.state.index[userId];
if (!status || !status.present) { if (!status || !status.present) {
return; return;
} }
status.present = false; status.present = false;
status.delta -= 1; status.delta -= 1;
var m = this.state.members.slice(); let m = this.state.members.slice();
m.splice(index, 1); m.splice(index, 1);
var sel = GroupManager.selectedContacts(m); const sel = GroupManager.selectedContacts(m);
var i = this.state.index; const i = this.state.index;
i[userId] = status; i[userId] = status;
this.setState({members: m, index: i, selectedContacts: sel}); this.setState({members: m, index: i, selectedContacts: sel});
@@ -107,7 +119,7 @@ class GroupManager extends React.Component {
static doContactFiltering(filter, values) { static doContactFiltering(filter, values) {
if (filter) { if (filter) {
for (var i=0; i<values.length; i++) { for (let i=0; i<values.length; i++) {
if (values[i].indexOf(filter) >= 0) { if (values[i].indexOf(filter) >= 0) {
return true; return true;
} }
@@ -154,7 +166,7 @@ class GroupManager extends React.Component {
<div className="panel-form-row"> <div className="panel-form-row">
<ChipInput <ChipInput
chips={this.state.members} chips={this.state.members}
required={this.props.requiredMember} staticMembers={this.state.staticMembers}
prompt="add members" prompt="add members"
filterFunc={this.handleContactFilter} filterFunc={this.handleContactFilter}
onChipRemoved={this.handleMemberRemoved} /> onChipRemoved={this.handleMemberRemoved} />
+7 -4
View File
@@ -4,6 +4,8 @@ import { FormattedMessage } from 'react-intl';
import AvatarUpload from './avatar-upload.jsx'; import AvatarUpload from './avatar-upload.jsx';
import TagManager from './tag-manager.jsx'; import TagManager from './tag-manager.jsx';
import { MAX_TITLE_LENGTH } from '../config.js';
export default class NewTopicGroup extends React.PureComponent { export default class NewTopicGroup extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -41,10 +43,11 @@ export default class NewTopicGroup extends React.PureComponent {
handleSubmit(e) { handleSubmit(e) {
e.preventDefault(); e.preventDefault();
if (this.state.fn && this.state.fn.trim()) {
this.props.onSubmit(this.state.fn.trim(), const fn = this.state.fn.trim().substring(0, MAX_TITLE_LENGTH);
this.state.imageDataUrl, this.state.private.trim(), const comment = this.state.private.trim().substring(0, MAX_TITLE_LENGTH);
this.state.tags); if (fn) {
this.props.onSubmit(fn, this.state.imageDataUrl, comment, this.state.tags);
} }
} }
+3 -3
View File
@@ -150,12 +150,12 @@ class PermissionsEditor extends React.Component {
</tbody></table> </tbody></table>
<br /> <br />
<div className="dialog-buttons"> <div className="dialog-buttons">
<button className="outline" onClick={this.handleCancel}>
<FormattedMessage id="button_cancel" />
</button>
<button className="blue" onClick={this.handleSubmit}> <button className="blue" onClick={this.handleSubmit}>
<FormattedMessage id="button_ok" /> <FormattedMessage id="button_ok" />
</button> </button>
<button className="white" onClick={this.handleCancel}>
<FormattedMessage id="button_cancel" />
</button>
</div> </div>
</div> </div>
); );
+4 -4
View File
@@ -121,13 +121,13 @@ export default class TagManager extends React.Component {
filterFunc={this.handleTagInput} /> filterFunc={this.handleTagInput} />
}</FormattedMessage> }</FormattedMessage>
{this.props.onSubmit || this.props.onCancel ? {this.props.onSubmit || this.props.onCancel ?
<div id="tag-manager-buttons" className="panel-form-row"> <div id="tag-manager-buttons" className="dialog-buttons panel-form-row">
<button className="outline" onClick={this.handleCancel}>
<FormattedMessage id="button_cancel" defautMessage="Cancel" description="Rejection button [Cancel]" />
</button>
<button className="blue" onClick={this.handleSubmit}> <button className="blue" onClick={this.handleSubmit}>
<FormattedMessage id="button_ok" defautMessage="OK" description="Confirmation button [OK]" /> <FormattedMessage id="button_ok" defautMessage="OK" description="Confirmation button [OK]" />
</button> </button>
<button className="white" onClick={this.handleCancel}>
<FormattedMessage id="button_cancel" defautMessage="Cancel" description="Rejection button [Cancel]" />
</button>
</div> </div>
: null} : null}
</div> </div>
+706 -3022
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
+6916
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+23 -8
View File
@@ -1,10 +1,13 @@
const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const path = require('path'); const path = require('path');
module.exports = { module.exports = (env, argv) => {
entry: [ const mode = argv.mode === 'production' ? 'prod' : 'dev';
path.resolve(__dirname, 'src/index.js'), return {
], entry: {
index: path.resolve(__dirname, 'src/index.js'),
},
devtool: 'source-map', devtool: 'source-map',
module: { module: {
rules: [ rules: [
@@ -17,6 +20,11 @@ module.exports = {
}, },
], ],
}, },
output: {
path: path.resolve(__dirname, 'umd'),
filename: `[name].${mode}.js`,
publicPath: '/umd/'
},
optimization: { optimization: {
minimizer: [ minimizer: [
// we specify a custom UglifyJsPlugin here to get source maps in production. // we specify a custom UglifyJsPlugin here to get source maps in production.
@@ -32,14 +40,21 @@ module.exports = {
] ]
}, },
performance: { performance: {
maxEntrypointSize: 280000, maxEntrypointSize: 262144,
maxAssetSize: 280000 maxAssetSize: 262144
}, },
plugins: [
new CopyPlugin([
{ from: `node_modules/tinode-sdk/umd/tinode.${mode}.js`, to: `tinode.${mode}.js` },
]),
],
externals: { externals: {
'react': 'React', 'react': 'React',
'react-dom': 'ReactDOM', 'react-dom': 'ReactDOM',
'react-intl': 'ReactIntl', 'react-intl': 'ReactIntl',
'firebase/app': 'firebase', 'firebase/app': 'firebase',
'firebase/messaging': ['firebase', 'messaging'] 'firebase/messaging': ['firebase', 'messaging'],
'tinode-sdk': 'Tinode',
}, },
}; };
}