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.map 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"],
"target": {
"namespace": "android_app",
"package_name": "co.tinode.tindroid",
"package_name": "co.tinode.tindroidx",
"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"],
"target": {
"namespace": "android_app",
"package_name": "co.tinode.tindroid",
"package_name": "co.tinode.tindroidx",
"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
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.
## Missing functionality
## Not done yet
* Previews not generated for videos, audio, links or docs.
* No end-to-end encryption.
* End-to-End encryption.
* Emoji support is weak.
* Mentions, hashtags.
* Replying or forwarding messages.
* Previews not generated for videos, audio, links or docs.
## Other
+102 -49
View File
@@ -9,9 +9,9 @@
/* Basic elements defined */
html, body {
height:100%;
width:100%;
font-size: 10pt;
height: 100%;
width: 100%;
font-size: 10pt; /* 13.33 px/rem */
color: #666;
overflow: hidden;
background-color: silver;
@@ -82,9 +82,9 @@ img.avatar {
button {
display: inline-block;
font-family: inherit;
font-size: 1.1rem;
height: 2.2em;
line-height: 2.2em;
font-size: 1.05rem;
height: 2.2rem;
line-height: 2.2rem;
font-weight: normal;
text-align: center;
vertical-align: middle;
@@ -92,11 +92,13 @@ button {
cursor: pointer;
border: 1px solid transparent;
white-space: nowrap;
padding-left: 2rem;
padding-right: 2rem;
border-radius: 2px;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.25);
padding-left: 1.2rem;
padding-right: 1.2rem;
border-radius: 4px;
box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.25);
text-transform: uppercase;
min-width: 5rem;
margin: 0 0.35rem 0 0.35rem;
}
button[disabled],
@@ -148,7 +150,7 @@ button.blue {
color: white;
}
button.blue:focus, button.blue:hover, button.blue:active {
background-color: #1976d2;
background-color: #2962ff;
}
button.white {
@@ -159,6 +161,16 @@ button.white:focus, button.white:hover, button.blue:active {
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 {
font-family: inherit;
font-size: 1.1rem;
@@ -222,8 +234,11 @@ input[type="password"].with-visibility {
margin-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 {
width: 0.6rem;
}
@@ -237,7 +252,12 @@ input[type="password"].with-visibility {
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 */
@@ -303,6 +323,40 @@ li.right:hover .menuTrigger {
/* 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 */
.blue {
color: #2196f3; /* blue 500 */
@@ -354,7 +408,7 @@ li.right:hover .menuTrigger {
}
/* Top-level application layout style: sidepanel + main view */
#noscript-alertbox {
#noscript-message {
position: relative;
display: flex;
flex-direction: column;
@@ -405,6 +459,8 @@ li.right:hover .menuTrigger {
}
#topic-view {
position: relative;
overflow-x: hidden;
display: flex;
flex-direction: column;
width: 100%;
@@ -552,6 +608,7 @@ table.permission-editor td.checkbox {
height: 4rem;
white-space: nowrap;
box-shadow: 0 1px 2px gray;
overflow-x: hidden;
z-index: 1; /* otherwise the shadow becomes invisible */
}
@@ -659,57 +716,57 @@ i.search {
}
/* Area which displays an error message */
.alert-box {
.info-box {
position: relative;
display: none;
overflow: hidden;
padding: 1rem 1.5rem 1rem 0.5rem;
}
.alert-box .cancel {
.info-box .cancel {
position: absolute;
right: 0.25rem;
top: 0.5rem;
}
.alert-box.error {
.info-box.error {
display: flex;
background-color: #ffcdd2;
border-bottom: 1px solid #ffbfc8;
color: #b71c1c;
}
.alert-box.warning {
.info-box.warning {
display: flex;
background-color: #fff9c4;
border-bottom: 1px solid #fff59d;
color: #6d4c41;
}
.alert-box.info {
.info-box.info {
display: flex;
background-color: #e1f5fe;
border-bottom: 1px solid #b3e5fc;
color: #0288d1;
}
.alert-box .cancel .material-icons {
.info-box .cancel .material-icons {
font-size: 1rem;
color: #757575;
}
.alert-box.error .cancel .material-icons {
.info-box.error .cancel .material-icons {
color: #b71c1c;
}
.alert-box .icon {
.info-box .icon {
display: block;
margin-right: 0.5rem;
}
.alert-box .icon .material-icons {
.info-box .icon .material-icons {
font-size: 1.5rem;
}
.alert-box.warning .icon .material-icons {
.info-box.warning .icon .material-icons {
color: #fbc02d;
}
.alert-box.error .icon .material-icons {
.info-box.error .icon .material-icons {
color: #ef5350;
}
.alert-box.info .icon .material-icons {
.info-box.info .icon .material-icons {
color: #0288d1;
}
/* List of contacts */
@@ -876,7 +933,6 @@ i.search {
.panel-form-row {
display: flex;
justify-content: flex-start;
align-items: center;
padding: 0.5rem 0.75rem;
line-height: 1.5;
@@ -897,13 +953,12 @@ i.search {
the top level panel */
.panel-form-row > .panel-form-column,
.panel-form-column > .panel-form-column {
padding: 0!important;
flex-grow: 1;
padding: 0!important;
flex-grow: 1;
}
.panel-form-column > .panel-form-row,
.panel-form-row > .panel-form-row
{
padding: 0!important;
.panel-form-row > .panel-form-row {
padding: 0!important;
}
/* Styling of avatar upload control */
@@ -1014,10 +1069,6 @@ i.search {
/* END of sidepanel styles */
/* Topic/messages view */
#topic-view {
position: relative;
}
#topic-caption-panel {
background-color: #b0bec5;
}
@@ -1044,6 +1095,10 @@ i.search {
line-height: 3rem;
}
#topic-title-group {
overflow-x: hidden;
}
#hide-message-view {
display: block;
margin-right: 0.35rem;
@@ -1560,45 +1615,43 @@ ul.tabbar li.active > a:after {
/* Badge with a label, like [you], [muted], etc */
.badge {
font-family: 'Roboto Mono', Courier, monospace, sans-serif;
font-size: 0.75rem;
border: 1px solid #666;
background-color: #eee;
font-size: 0.8rem;
border: 1px solid silver;
background-color: #e8e8e8;
color: #666;
padding: 0 0.15rem 0 0.15rem;
margin-left: 0.1rem;
margin-right: 0.1rem;
margin-bottom: 0.1rem;
border-radius: 0.15rem;
padding: 0.05rem 0.25rem 0.05rem 0.25rem;
margin: 0.05rem 0.1rem 0.05rem 0.1rem;
border-radius: 0.2rem;
display: inline-block;
align-self: flex-start;
}
.badge.green {
border-color: #383;
border-color: #9c9;
background-color: #efe;
color: #383;
}
.badge.yellow {
border-color: #660;
border-color: #cc9;
background-color: #ffe;
color: #660;
}
.badge.blue {
border-color: #339;
border-color: #99c;
background-color: #eef;
color: #339;
}
.badge.red {
border-color: #933;
border-color: #c99;
background-color: #fee;
color: #933;
}
.badge.magenta {
border-color: #939;
border-color: #c9c;
background-color: #fef;
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:title" content="Tinode Web Development">
<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="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" />
@@ -27,19 +28,18 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<!-- Styles for local elements -->
<link rel="stylesheet" href="css/base.css" />
<!-- ReactJs scripts -->
<script crossorigin="anonymous" src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<!-- React-Intl -->
<script crossorigin="anonymous" src="https://unpkg.com/prop-types@15/prop-types.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/react-intl@2/dist/react-intl.js"></script>
<!-- Firebase scripts -->
<script crossorigin="anonymous" src="https://unpkg.com/firebase@6.4.2/firebase-app.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/firebase@6.4.2/firebase-messaging.js"></script>
<!-- ReactJS -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@16/umd/react.development.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@16/umd/react-dom.development.js"></script>
<!-- React-Intl for translations -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@3/dist/react-intl.js"></script>
<!-- Firebase for push notifications -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@6.6.2/firebase-app.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@6.6.2/firebase-messaging.js"></script>
</head>
<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.
TinodeWeb does not work without JavaScript. Please enable JavaScript by changing your browser options,
then <a href="">try again</a>.
@@ -47,6 +47,7 @@
</div></noscript>
<div id="mountPoint"></div>
<script src="firebase-init.js"></script>
<script src="umd/tinode.dev.js"></script>
<script src="umd/index.dev.js"></script>
</body>
</html>
+9 -7
View File
@@ -13,6 +13,8 @@
<meta name="og:url" content="https://web.tinode.co/">
<meta name="og:title" content="Tinode Web">
<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="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" />
@@ -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/icon?family=Material+Icons" />
<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://unpkg.com/react-dom@16.9.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://unpkg.com/react-intl@2.9.0/dist/react-intl.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/firebase@6.4.2/firebase-app.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/firebase@6.4.2/firebase-messaging.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://cdn.jsdelivr.net/npm/react-dom@16.11.0/umd/react-dom.production.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://cdn.jsdelivr.net/npm/firebase@6.6.2/firebase-app.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@6.6.2/firebase-messaging.js"></script>
</head>
<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.
TinodeWeb does not work without JavaScript. Please enable JavaScript by changing your browser options,
then <a href="">try again</a>.</div></div></noscript>
<div id="mountPoint"></div>
<script src="firebase-init.js"></script>
<script src="umd/tinode.prod.js"></script>
<script src="umd/index.prod.js"></script>
</body>
</html>
+924 -606
View File
File diff suppressed because it is too large Load Diff
+17 -13
View File
@@ -1,7 +1,7 @@
{
"name": "tinode-webapp",
"description": "Tinode messenger for the web",
"version": "0.16.1",
"version": "0.16.2-rc4",
"repository": {
"type": "git",
"url": "https://github.com/tinode/example-react-js.git"
@@ -9,11 +9,12 @@
"scripts": {
"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:dev": "webpack --mode development --output ./umd/index.dev.js",
"build:prod": "webpack --mode production --output ./umd/index.prod.js",
"build:dev": "webpack --mode development",
"build:prod": "webpack --mode production",
"build:i18n": "node scripts/flatten-messages.js",
"vers": "node scripts/gen-version.js"
},
"browserslist": "> 0.5%, not IE 11",
"files": [
"src/index.js",
"src/config.js",
@@ -25,6 +26,8 @@
"umd/index.prod.js.map",
"umd/index.dev.js",
"umd/index.dev.js.map",
"umd/tinode.prod.js",
"umd/tinode.dev.js",
"audio/msg.mp3",
"css/base.css",
"css/base.min.css",
@@ -68,20 +71,21 @@
"firebase": "^6.4.2",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-intl": "^2.9.0",
"tinode-sdk": "^0.16.1"
"react-intl": "^3.2.1",
"tinode-sdk": "^0.16.2-rc3"
},
"devDependencies": {
"@babel/core": "^7.5.4",
"@babel/preset-env": "^7.5.4",
"@babel/core": "^7.6.0",
"@babel/preset-env": "^7.6.0",
"@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.6",
"babel-plugin-react-intl": "^4.1.0",
"browserslist": "^4.6.6",
"babel-plugin-react-intl": "^4.1.19",
"browserslist": "^4.7.0",
"copy-webpack-plugin": "^5.0.4",
"cssnano": "^4.1.10",
"postcss": "^7.0.17",
"uglifyjs-webpack-plugin": "^2.1.3",
"webpack": "^4.35.3",
"webpack-cli": "^3.3.6"
"postcss": "^7.0.18",
"uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "^4.40.2",
"webpack-cli": "^3.3.9"
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
// Must be located at the root.
importScripts('https://unpkg.com/firebase@6.4.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-app.js');
importScripts('https://cdn.jsdelivr.net/npm/firebase@6.6.2/firebase-messaging.js');
importScripts('firebase-init.js');
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.
export const DEFAULT_HOST = KNOWN_HOSTS.hosted;
// Enable console logging.
export const LOGGING_ENABLED = true;
// Minimum time between two keypress notifications, milliseconds.
export const KEYPRESS_DELAY = 3*1000;
// 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.
export const MIN_TAG_LENGTH = 4;
// Access mode for subscriptions initiated by the current user.
export const DEFAULT_ACCESS_MODE = 'JRWPS';
// Access mode for P2P subscriptions initiated by the current user.
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.
export const NO_ACCESS_MODE = 'N';
@@ -64,11 +69,14 @@ export const MAX_IMAGE_DIM = 768;
// hidden under "+X more"
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".
export const LINK_CONTACT_US = 'email:info@tinode.co';
// 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.
export const LINK_TERMS_OF_SERVICE = 'https://tinode.co/terms.html';
+12 -2
View File
@@ -24,6 +24,16 @@
"granted_permissions": "Granted",
"menu_item_edit_permissions": "Edit permissions",
"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",
"label_topic_name": "Name",
"label_private": "Private comment",
@@ -36,9 +46,7 @@
"label_default_access": "Default access mode:",
"label_group_members": "Group members:",
"button_add_members": "Add members",
"button_leave": "Leave",
"group_has_no_members": "No members",
"action_leave_chat": "Leave",
"login_prompt": "Login",
"password_prompt": "Password",
"stay_logged_in": "Stay logged in",
@@ -94,8 +102,10 @@
"menu_item_mute": "Mute",
"menu_item_unmute": "Unmute",
"menu_item_delete_topic": "Delete",
"topic_delete_warning": "Are you sure you want to delete this conversation?",
"menu_item_unblock": "Unblock",
"menu_item_block": "Block",
"topic_block_warning": "Are you sure you want to block this conversation?",
"menu_item_member_delete": "Remove",
"menu_item_archive_topic": "Archive",
"action_cancel": "cancel",
+17 -7
View File
@@ -2,7 +2,7 @@
"contacts_not_found": "Чатов нет<br />¯∖_(ツ)_/¯",
"full_name_prompt": "Полное имя, напр. Иван Петров",
"email_prompt": "Email, напр. ivan@example.com",
"button_sign_up": "Создать счет",
"button_sign_up": "Создать аккаунт",
"label_your_name": "Ваше имя",
"label_password": "Пароль",
"password_unchanged_prompt": "Не изменен",
@@ -11,7 +11,7 @@
"label_message_sound": "Звук нового сообщения:",
"label_push_notifications": "Уведомления:",
"label_push_notifications_disabled": "Уведомления (требуют HTTPS):",
"title_tag_manager": "Таги для поиска",
"title_tag_manager": "Теги для поиска",
"button_logout": "Выйти",
"login_prompt": "Логин",
"password_prompt": "Пароль",
@@ -34,12 +34,12 @@
"sidepanel_title_login": "Авторизация",
"sidepanel_title_register": "Зарегистрироваться",
"sidepanel_title_settings": "Настройки",
"sidepanel_title_edit_account": "Редактировать счет",
"sidepanel_title_edit_account": "Редактировать аккаунт",
"sidepanel_title_newtpk": "Новый чат",
"sidepanel_title_cred": "Подтвердить",
"sidepanel_title_reset": "Сменить пароль",
"tags_not_found": "Тагов нет. Добавьте",
"tags_editor_no_tags": "Добавьте таги",
"tags_not_found": "Тегов нет. Добавьте",
"tags_editor_no_tags": "Добавьте теги",
"title_manage_tags": "Редактировать",
"message_sending": "в пути...",
"message_sending_failed": "ошибка",
@@ -86,7 +86,6 @@
"label_default_access": "Права по умолчанию:",
"label_group_members": "Участники чата:",
"button_add_members": "Добавить",
"button_leave": "Отписаться",
"group_has_no_members": "Нет участников",
"action_leave_chat": "Уйти из чата",
"menu_item_info": "Информация",
@@ -138,5 +137,16 @@
"label_legal": "Прочее",
"link_contact_us": "Связаться с нами",
"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_group_members": "群组成员:",
"button_add_members": "添加成员",
"button_leave": "离开",
"group_has_no_members": "无成员",
"action_leave_chat": "离开",
"login_prompt": "登录",
@@ -138,5 +137,16 @@
"label_legal": "法律东西",
"link_contact_us": "联系我们",
"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 ReactDOM from 'react-dom';
import { IntlProvider, addLocaleData } from 'react-intl';
import ruData from 'react-intl/locale-data/ru';
addLocaleData(ruData);
import { IntlProvider } from 'react-intl';
import allMessages from './messages.json';
import TinodeWeb from './views/tinode-web.jsx';
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) ||
(navigator.languages && navigator.languages[0]) ||
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 =
allMessages[language] ||
allMessages[baseLanguage] ||
+30 -10
View File
@@ -25,6 +25,16 @@
"granted_permissions": "Granted",
"menu_item_edit_permissions": "Edit permissions",
"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",
"label_topic_name": "Name",
"label_private": "Private comment",
@@ -37,9 +47,7 @@
"label_default_access": "Default access mode:",
"label_group_members": "Group members:",
"button_add_members": "Add members",
"button_leave": "Leave",
"group_has_no_members": "No members",
"action_leave_chat": "Leave",
"login_prompt": "Login",
"password_prompt": "Password",
"stay_logged_in": "Stay logged in",
@@ -95,8 +103,10 @@
"menu_item_mute": "Mute",
"menu_item_unmute": "Unmute",
"menu_item_delete_topic": "Delete",
"topic_delete_warning": "Are you sure you want to delete this conversation?",
"menu_item_unblock": "Unblock",
"menu_item_block": "Block",
"topic_block_warning": "Are you sure you want to block this conversation?",
"menu_item_member_delete": "Remove",
"menu_item_archive_topic": "Archive",
"action_cancel": "cancel",
@@ -154,7 +164,7 @@
"label_message_sound": "Звук нового сообщения:",
"label_push_notifications": "Уведомления:",
"label_push_notifications_disabled": "Уведомления (требуют HTTPS):",
"title_tag_manager": "Таги для поиска",
"title_tag_manager": "Теги для поиска",
"button_logout": "Выйти",
"login_prompt": "Логин",
"password_prompt": "Пароль",
@@ -181,8 +191,8 @@
"sidepanel_title_newtpk": "Новый чат",
"sidepanel_title_cred": "Подтвердить",
"sidepanel_title_reset": "Сменить пароль",
"tags_not_found": "Тагов нет. Добавьте",
"tags_editor_no_tags": "Добавьте таги",
"tags_not_found": "Тегов нет. Добавьте",
"tags_editor_no_tags": "Добавьте теги",
"title_manage_tags": "Редактировать",
"message_sending": "в пути...",
"message_sending_failed": "ошибка",
@@ -229,7 +239,6 @@
"label_default_access": "Права по умолчанию:",
"label_group_members": "Участники чата:",
"button_add_members": "Добавить",
"button_leave": "Отписаться",
"group_has_no_members": "Нет участников",
"action_leave_chat": "Уйти из чата",
"menu_item_info": "Информация",
@@ -281,9 +290,18 @@
"label_legal": "Прочее",
"link_contact_us": "Связаться с нами",
"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})",
"contacts_not_found": "你尚无会话<br />¯∖_(ツ)_/¯",
"full_name_prompt": "全名,例如张伟",
@@ -315,7 +333,6 @@
"label_default_access": "默认权限模式:",
"label_group_members": "群组成员:",
"button_add_members": "添加成员",
"button_leave": "离开",
"group_has_no_members": "无成员",
"action_leave_chat": "离开",
"login_prompt": "登录",
@@ -423,6 +440,9 @@
"label_legal": "法律东西",
"link_contact_us": "联系我们",
"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.
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) {
if (prevProps.chatList != this.props.chatList) {
if (prevProps.chatList != this.props.chatList || prevProps.archive != this.props.archive) {
const newState = ContactsView.deriveStateFromProps(this.props);
this.setState(newState);
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 { vcard } from '../lib/utils.js';
import { MAX_TITLE_LENGTH } from '../config.js';
export default class CreateAccountView extends React.PureComponent {
constructor(props) {
super(props);
@@ -59,12 +61,11 @@ export default class CreateAccountView extends React.PureComponent {
handleSubmit(e) {
e.preventDefault();
// TODO: check email for validity
this.setState({errorCleared: false});
this.props.onCreateAccount(
this.state.login.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});
}
+7 -4
View File
@@ -10,7 +10,7 @@ import InPlaceEdit from '../widgets/in-place-edit.jsx';
import PermissionsEditor from '../widgets/permissions-editor.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 { arrayEqual, asEmail, asPhone, vcard } from '../lib/utils.js';
@@ -70,8 +70,11 @@ export default class EditAccountView extends React.Component {
}
handleFullNameUpdate(fn) {
this.setState({fullName: fn});
this.props.onUpdateAccount(undefined, vcard(fn, null));
fn = fn.trim().substring(0, MAX_TITLE_LENGTH);
if (fn) {
this.setState({fullName: fn});
this.props.onUpdateAccount(undefined, vcard(fn, null));
}
}
handlePasswordUpdate(pwd) {
@@ -291,7 +294,7 @@ export default class EditAccountView extends React.Component {
<div className="hr" />
<div className="panel-form-column">
<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]" />
</a>
</div>
+214 -92
View File
@@ -15,7 +15,7 @@ import MoreButton from '../widgets/more-button.jsx';
import PermissionsEditor from '../widgets/permissions-editor.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 { arrayEqual, vcard } from '../lib/utils.js';
@@ -40,7 +40,57 @@ const messages = defineMessages({
id: 'label_other_user',
defaultMessage: 'Other',
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 {
@@ -52,6 +102,7 @@ class InfoView extends React.Component {
owner: false,
admin: false,
sharer: false,
deleter: false,
muted: false,
address: null,
groupTopic: undefined,
@@ -91,7 +142,10 @@ class InfoView extends React.Component {
this.handleShowAddMembers = this.handleShowAddMembers.bind(this);
this.handleHideAddMembers = this.handleHideAddMembers.bind(this);
this.handleMemberUpdateRequest = this.handleMemberUpdateRequest.bind(this);
this.handleDeleteMessages = this.handleDeleteMessages.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.handleMoreInfo = this.handleMoreInfo.bind(this);
this.handleTagsUpdated = this.handleTagsUpdated.bind(this);
@@ -168,6 +222,7 @@ class InfoView extends React.Component {
owner: acs && acs.isOwner(),
admin: acs && acs.isAdmin(),
sharer: acs && acs.isSharer(),
deleter: acs && acs.isDeleter(),
muted: acs && acs.isMuted(),
fullName: topic.public ? topic.public.fn : undefined,
@@ -222,17 +277,18 @@ class InfoView extends React.Component {
}
handleFullNameUpdate(fn) {
fn = fn.trim();
fn = fn.trim().substring(0, MAX_TITLE_LENGTH);
if (this.state.fullName !== fn) {
this.setState({fullName: fn});
this.props.onTopicDescUpdate(this.props.topic, vcard(fn, null), null);
}
}
handlePrivateUpdate(priv) {
if (this.state.priv !== priv) {
this.setState({private: priv});
this.props.onTopicDescUpdate(this.props.topic, null, priv || Tinode.DEL_CHAR);
handlePrivateUpdate(comment) {
comment = comment.trim().substring(0, MAX_TITLE_LENGTH);
if (this.state.private !== comment) {
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});
}
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) {
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) {
@@ -422,6 +525,7 @@ class InfoView extends React.Component {
<GroupManager
members={this.state.contactList}
requiredMember={this.props.myUserId}
keepInitialMembers={!this.state.admin && !this.state.owner}
myUserId={this.props.myUserId}
contacts={this.props.searchableContacts}
onCancel={this.handleHideAddMembers}
@@ -498,45 +602,45 @@ class InfoView extends React.Component {
}</FormattedMessage>
{this.state.moreInfoExpanded ?
<div className="panel-form-column">
<div className="panel-form-row">
<label><FormattedMessage id="label_user_id" /></label>
<tt>{this.state.address}</tt>
</div>
{this.state.groupTopic ?
<div className="panel-form-row">
<label>
<FormattedMessage id="label_your_permissions" defaultMessage="Your permissions:"
description="Label for current user permissions" />
</label>
<tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'want')}>
{this.state.access}
</tt>
<label><FormattedMessage id="label_user_id" /></label>
<tt>{this.state.address}</tt>
</div>
:
<div>
<div>
<label className="small">
<FormattedMessage id="label_permissions" defaultMessage="Permissions:"
description="Section title" />
{this.state.groupTopic ?
<div className="panel-form-row">
<label>
<FormattedMessage id="label_your_permissions" defaultMessage="Your permissions:"
description="Label for current user permissions" />
</label>
</div>
<div className="quoted">
<div>
<FormattedMessage id="label_you" defaultMessage="You:"
description="Label for the current user" /> &nbsp;<tt className="clickable"
<tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'want')}>
{this.state.access}
</tt></div>
<div>{this.state.fullName ? this.state.fullName : formatMessage(messages.other_user)}:
&nbsp;<tt className="clickable" onClick={this.handleLaunchPermissionsEditor.bind(this, 'given')}>
{this.state.modeGiven2}
</tt>
</tt>
</div>
:
<div>
<div>
<label className="small">
<FormattedMessage id="label_permissions" defaultMessage="Permissions:"
description="Section title" />
</label>
</div>
<div className="quoted">
<div>
<FormattedMessage id="label_you" defaultMessage="You:"
description="Label for the current user" /> &nbsp;<tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'want')}>
{this.state.access}
</tt></div>
<div>{this.state.fullName ? this.state.fullName : formatMessage(messages.other_user)}:
&nbsp;<tt className="clickable" onClick={this.handleLaunchPermissionsEditor.bind(this, 'given')}>
{this.state.modeGiven2}
</tt>
</div>
</div>
</div>
</div>
}
{this.state.sharer && (this.state.auth || this.state.anon) ?
}
{this.state.sharer && (this.state.auth || this.state.anon) ?
<div>
<div>
<label className="small">
@@ -575,62 +679,80 @@ class InfoView extends React.Component {
</div>
<div className="hr" />
{this.state.owner ?
<FormattedMessage id="title_tag_manager">{
(tags) => <TagManager
title={tags}
tags={this.state.tags}
activated={false}
onSubmit={this.handleTagsUpdated} />
}</FormattedMessage>
<>
<FormattedMessage id="title_tag_manager">{
(tags) => <TagManager
title={tags}
tags={this.state.tags}
activated={false}
onSubmit={this.handleTagsUpdated} />
}</FormattedMessage>
<div className="hr" />
</>
:
null
}
{this.state.owner ? <div className="hr" /> : null }
{this.state.groupTopic ?
<div className="panel-form-column">
<div className="panel-form-row">
<label className="small">
<FormattedMessage id="label_group_members" defaultMessage="Group members:"
description="Section title or label" />
</label>
</div>
<div className="panel-form-row">
{this.state.sharer ?
<a href="#" className="flat-button" onClick={this.handleShowAddMembers}>
<i className="material-icons">person_add</i> <FormattedMessage id="button_add_members"
defaultMessage="Add members" description="Flat button [Add members] (to topic)" />
</a>
: 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>
<FormattedMessage id="group_has_no_members" defaultMessage="No members"
description="Shown in place of group members">{
(no_members) => <ContactList
contacts={this.state.contactList}
myUserId={this.props.myUserId}
emptyListMessage={no_members}
topicSelected={this.state.selectedContact}
showOnline={false}
showUnread={false}
showMode={true}
noScroll={true}
onTopicSelected={this.handleMemberSelected}
showContextMenu={this.state.admin ? this.handleContextMenu : false}
/>
}</FormattedMessage>
</div>
:
<div className="panel-form-row">
<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" />
<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>
</div>
:
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 ?
<>
<div className="hr" />
<div className="panel-form-column">
<div className="panel-form-row">
<label className="small">
<FormattedMessage id="label_group_members" defaultMessage="Group members:"
description="Section title or label" />
</label>
</div>
<div className="panel-form-row">
{this.state.sharer ?
<a href="#" className="flat-button" onClick={this.handleShowAddMembers}>
<i className="material-icons">person_add</i> &nbsp;<FormattedMessage id="button_add_members"
defaultMessage="Add members" description="Flat button [Add members] (to topic)" />
</a>
: null}
</div>
<FormattedMessage id="group_has_no_members" defaultMessage="No members"
description="Shown in place of group members">{
(no_members) => <ContactList
contacts={this.state.contactList}
myUserId={this.props.myUserId}
emptyListMessage={no_members}
topicSelected={this.state.selectedContact}
showOnline={false}
showUnread={false}
showMode={true}
noScroll={true}
onTopicSelected={this.handleMemberSelected}
showContextMenu={this.state.admin ? this.handleContextMenu : false}
/>
}</FormattedMessage>
</div>
</>
:
null
}
</div>
}
+5 -7
View File
@@ -14,7 +14,7 @@ import LoadSpinner from '../widgets/load-spinner.jsx';
import LogoView from './logo-view.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 { shortDateFormat } from '../lib/strformat.js';
@@ -85,7 +85,6 @@ class MessagesView extends React.Component {
}
componentWillUnmount() {
this.leave(this.state.topic);
if (this.messagesScroller) {
this.messagesScroller.removeEventListener('scroll', this.handleScrollEvent);
}
@@ -104,7 +103,7 @@ class MessagesView extends React.Component {
const topic = this.props.tinode.getTopic(this.state.topic);
if (this.state.topic != prevState.topic) {
if (prevState.topic) {
if (prevState.topic && !Tinode.isNewGroupTopicName(prevState.topic)) {
this.leave(prevState.topic);
this.props.readTimerHandler(null);
}
@@ -379,9 +378,9 @@ class MessagesView extends React.Component {
// Regenerate messages list
const topic = this.props.tinode.getTopic(this.state.topic);
const newState = {messages: []};
topic.messages(function(m) {
topic.messages((m) => {
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.setState(newState);
}
@@ -491,7 +489,7 @@ class MessagesView extends React.Component {
handleEnablePeer(e) {
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() {
+4 -1
View File
@@ -1,8 +1,9 @@
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import SideNavbar from '../widgets/side-navbar.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 CreateAccountView from './create-account-view.jsx';
@@ -105,6 +106,8 @@ class SidepanelView extends React.Component {
actionText={this.props.errorActionText}
onClearError={this.props.onError} />
<LoadSpinner show={this.props.loadSpinnerVisible} />
{view === 'login' ?
<LoginView
login={this.props.login}
+110 -15
View File
@@ -8,13 +8,15 @@ import 'firebase/messaging';
import Tinode from 'tinode-sdk';
import Alert from '../widgets/alert.jsx';
import ContextMenu from '../widgets/context-menu.jsx';
import InfoView from './info-view.jsx';
import MessagesView from './messages-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 { detectServerAddress, isLocalHost, isSecureConnection } from '../lib/host-name.js';
import LocalStorageUtil from '../lib/local-storage.js';
@@ -98,11 +100,14 @@ class TinodeWeb extends React.Component {
this.handleChangePermissions = this.handleChangePermissions.bind(this);
this.handleTagsUpdated = this.handleTagsUpdated.bind(this);
this.handleLogout = this.handleLogout.bind(this);
this.handleDeleteMessagesRequest = this.handleDeleteMessagesRequest.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.defaultTopicContextMenu = this.defaultTopicContextMenu.bind(this);
this.handleHideContextMenu = this.handleHideContextMenu.bind(this);
this.handleShowAlert = this.handleShowAlert.bind(this);
this.handleShowInfoView = this.handleShowInfoView.bind(this);
this.handleHideInfoView = this.handleHideInfoView.bind(this);
this.handleMemberUpdateRequest = this.handleMemberUpdateRequest.bind(this);
@@ -138,8 +143,8 @@ class TinodeWeb extends React.Component {
sidePanelSelected: 'login',
sidePanelTitle: null,
sidePanelAvatar: null,
dialogSelected: null,
contextMenuVisible: false,
loadSpinnerVisible: false,
login: '',
password: '',
myUserId: null,
@@ -159,6 +164,10 @@ class TinodeWeb extends React.Component {
contextMenuParams: null,
contextMenuItems: [],
// Modal alert dialog.
alertVisible: false,
alertParams: {},
// Chats as shown in the ContactsView
chatList: [],
// 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.
static tnSetup(serverAddress, transport) {
let tinode = new Tinode(APP_NAME, serverAddress, API_KEY, transport, isSecureConnection());
tinode.enableLogging(true, true);
tinode.enableLogging(LOGGING_ENABLED, true);
return tinode;
}
@@ -351,7 +360,12 @@ class TinodeWeb extends React.Component {
// User clicked Login button in the side panel.
handleLoginRequest(login, password) {
this.setState({loginDisabled: true, login: login, password: password});
this.setState({
loginDisabled: true,
login: login,
password: password,
loadSpinnerVisible: true
});
this.handleError('', null);
if (this.tinode.isConnected()) {
@@ -419,7 +433,6 @@ class TinodeWeb extends React.Component {
connected: false,
ready: false,
topicSelectedOnline: false,
dialogSelected: null,
errorText: err && err.message ? err.message : "Disconnected",
errorLevel: err && err.message ? 'err' : 'warn',
loginDisabled: false,
@@ -449,6 +462,7 @@ class TinodeWeb extends React.Component {
if (promise) {
promise.then((ctrl) => {
if (ctrl.code >= 300 && ctrl.text === 'validate credentials') {
this.setState({loadSpinnerVisible: false});
if (cred) {
this.handleError("Code does not match", 'warn');
}
@@ -458,7 +472,12 @@ class TinodeWeb extends React.Component {
}
}).catch((err) => {
// 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');
localStorage.removeItem('auth-token');
HashNavigation.navigateTo('');
@@ -505,9 +524,12 @@ class TinodeWeb extends React.Component {
withCred().
build()
).catch((err) => {
this.tinode.disconnect();
localStorage.removeItem('auth-token');
this.handleError(err.message, 'err');
HashNavigation.navigateTo('');
}).finally(() => {
this.setState({loadSpinnerVisible: false});
});
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, 'contacts'));
}
@@ -566,11 +588,12 @@ class TinodeWeb extends React.Component {
this.setState({topicSelectedAcs: cont.acs});
}
} 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 {
// TODO(gene): handle other types of notifications:
// * 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);
}
}
@@ -984,15 +1007,16 @@ class TinodeWeb extends React.Component {
// Request to start a new topic. New P2P topic requires peer's name.
handleNewTopicRequest(peerName, pub, priv, tags) {
const topicName = peerName || this.tinode.newGroupTopicName();
const params = {
_topicName: topicName,
};
if (peerName) {
// 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.
params.desc = {defacs: {auth: DEFAULT_ACCESS_MODE}};
params.desc = {defacs: {auth: DEFAULT_P2P_ACCESS_MODE}};
} else {
params.desc = {public: pub, private: {comment: priv}};
params.tags = tags;
@@ -1080,6 +1104,18 @@ class TinodeWeb extends React.Component {
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) {
const topic = this.tinode.getTopic(topicName);
if (!topic) {
@@ -1094,8 +1130,39 @@ class TinodeWeb extends React.Component {
});
}
handleDialogCancel() {
this.setState({dialogSelected: null});
handleBlockTopicRequest(topicName) {
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) {
@@ -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() {
HashNavigation.navigateTo(HashNavigation.addUrlParam(window.location.hash, 'info', true));
this.setState({showInfoPanel: true});
@@ -1248,6 +1329,7 @@ class TinodeWeb extends React.Component {
params={this.state.contextMenuParams}
items={this.state.contextMenuItems}
hide={this.handleHideContextMenu}
onShowAlert={this.handleShowAlert}
onAction={this.handleContextMenuAction}
onTopicRemoved={(topicName) => {
if (topicName == this.state.topicSelected) {
@@ -1258,7 +1340,15 @@ class TinodeWeb extends React.Component {
:
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
tinode={this.tinode}
connected={this.state.connected}
@@ -1270,6 +1360,7 @@ class TinodeWeb extends React.Component {
login={this.state.login}
myUserId={this.state.myUserId}
loginDisabled={this.state.loginDisabled}
loadSpinnerVisible={this.state.loadSpinnerVisible}
errorText={this.state.errorText}
errorLevel={this.state.errorLevel}
@@ -1365,9 +1456,13 @@ class TinodeWeb extends React.Component {
onTopicDescUpdate={this.handleTopicUpdateRequest}
onCancel={this.handleHideInfoView}
onShowAlert={this.handleShowAlert}
onChangePermissions={this.handleChangePermissions}
onMemberUpdateRequest={this.handleMemberUpdateRequest}
onDeleteMessages={this.handleDeleteMessagesRequest}
onLeaveTopic={this.handleLeaveUnsubRequest}
onBlockTopic={this.handleBlockTopicRequest}
onReportTopic={this.handleReportTopic}
onAddMember={this.handleManageGroupMembers}
onTopicTagsUpdate={this.handleTagsUpdated}
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) {
return {
placeholder: props.chips ? '' : props.prompt,
sortedChips: ChipInput.sortChips(props.chips, props.required),
sortedChips: ChipInput.sortChips(props.chips, props.staticMembers),
chipIndex: ChipInput.indexChips(props.chips)
};
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.chips != this.props.chips ||
prevProps.required != this.props.required ||
prevProps.staticMembers != this.props.staticMembers ||
prevProps.prompt != this.props.prompt) {
this.setState(ChipInput.deriveStateFromProps(this.props));
}
@@ -44,7 +44,7 @@ export default class ChipInput extends React.Component {
static indexChips(chips) {
const index = {};
let count = 0;
chips.map(function(item) {
chips.map((item) => {
index[item.user] = count;
count ++;
});
@@ -55,8 +55,8 @@ export default class ChipInput extends React.Component {
static sortChips(chips, keep) {
const required = [];
const normal = [];
chips.map(function(item) {
if (item.user === keep) {
chips.map((item) => {
if (keep && keep.includes(item.user)) {
required.push(item);
} else {
normal.push(item);
@@ -95,8 +95,8 @@ export default class ChipInput extends React.Component {
handleKeyDown(e) {
if (e.key === 'Backspace') {
if (this.state.input.length == 0 && this.state.sortedChips.length > 0) {
var at = this.state.sortedChips.length - 1;
if (this.state.sortedChips[at].user !== this.props.required) {
const at = this.state.sortedChips.length - 1;
if (this.state.sortedChips[at].user !== this.props.staticMembers) {
this.removeChipAt(at);
}
}
@@ -114,6 +114,7 @@ export default class ChipInput extends React.Component {
render() {
const chips = [];
let count = 0;
const staticMembers = this.props.staticMembers || [];
this.state.sortedChips.map((item) => {
chips.push(
<Chip
@@ -122,7 +123,7 @@ export default class ChipInput extends React.Component {
title={item.public ? item.public.fn : undefined}
noAvatar={this.props.avatarDisabled}
topic={item.user}
required={item.user === this.props.required}
required={staticMembers.includes(item.user)}
invalid={item.invalid}
index={count}
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} />
</div>
{this.props.comment ? <div className="contact-comment">{this.props.comment}</div> : null}
<><ContactBadges badges={badges} /></>
<span><ContactBadges badges={badges} /></span>
</div>
{this.props.showContextMenu ?
<span className="menuTrigger">
+63 -32
View File
@@ -47,15 +47,26 @@ const messages = defineMessages({
defaultMessage: 'Delete',
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: {
id: 'menu_item_unblock',
defaultMessage: 'Unblock',
description: 'Unblock user'
description: 'Unblock topic or user'
},
// Generic string suitable for either topic or user.
block: {
id: 'menu_item_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: {
id: 'menu_item_member_delete',
@@ -90,14 +101,28 @@ class ContextMenu extends React.Component {
id: 'messages_clear',
title: formatMessage(messages.clear_messages),
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': {
id: 'messages_clear_hard',
title: formatMessage(messages.clear_for_all),
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': {
@@ -133,26 +158,44 @@ class ContextMenu extends React.Component {
id: 'topic_block',
title: formatMessage(messages.block),
handler: (params, errorHandler) => {
return this.topicPermissionSetter('-JP', params, errorHandler).then((ctrl) => {
this.props.onTopicRemoved(params.topicName);
return ctrl;
});
return props.onShowAlert(
formatMessage({id: 'menu_item_block'}), // title
formatMessage(messages.topic_block_warning), // content
(() => {
return this.topicPermissionSetter('-JP', params, errorHandler).then((ctrl) => {
this.props.onTopicRemoved(params.topicName);
return ctrl;
});
}),
null, // "OK"
true, // Show Reject button
null // "Cancel"
);
}
},
'topic_delete': {
id: 'topic_delete',
title: formatMessage(messages.topic_delete),
handler: (params, errorHandler) => {
const topic = this.props.tinode.getTopic(params.topicName);
if (!topic) {
console.log("Topic not found: ", params.topicName);
return;
}
return topic.delTopic().catch((err) => {
if (errorHandler) {
errorHandler(err.message, 'err');
}
});
return props.onShowAlert(
formatMessage({id: 'menu_item_delete_topic'}), // title
formatMessage(messages.topic_delete_warning), // content
(() => {
const topic = this.props.tinode.getTopic(params.topicName);
if (!topic) {
console.log("Topic not found: ", params.topicName);
return;
}
return topic.delTopic().catch((err) => {
if (errorHandler) {
errorHandler(err.message, 'err');
}
});
}),
null, // "OK"
true, // Show Reject button
null // "Cancel"
);
}
},
'topic_archive': {
@@ -277,7 +320,7 @@ class ContextMenu extends React.Component {
return;
}
// Can't cancel. Delete instead.
let promise = all ?
const promise = all ?
topic.delMessagesAll(hard) :
topic.delMessagesList([params.seq], hard);
@@ -296,19 +339,7 @@ class ContextMenu extends React.Component {
return;
}
let am, user;
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}});
let result = topic.updateMode(params.user, mode);
if (errorHandler) {
result = result.catch((err) => {
errorHandler(err.message, 'err');
+10 -6
View File
@@ -31,18 +31,22 @@ export default class ErrorPanel extends React.PureComponent {
render() {
const icons = {err: 'error', warn: 'warning', info: 'info'}
const level = icons[this.props.level] || '';
const className = 'alert-box ' + level;
const className = 'info-box ' + level;
return (
<div className={className}>
<div className="icon"><i className="material-icons">{level}</i></div>
<>
<span>
<span dangerouslySetInnerHTML={{__html: this.props.text}} />
{this.props.action ?
<a href="#" onClick={(e) => {e.preventDefault(); this.props.action();}}>
{this.props.actionText}
</a>
<>
&#32;<a href="#"
style={{ whiteSpace: 'nowrap' }}
onClick={(e) => {e.preventDefault(); this.props.action();}}>
{this.props.actionText}
</a>
</>
: null}
</>
</span>
<div className="cancel"><MenuCancel onCancel={this.hide} /></div>
</div>
);
+23 -11
View File
@@ -23,8 +23,10 @@ class GroupManager extends React.Component {
super(props);
this.state = {
// Array of topic members
members: props.members,
index: GroupManager.indexMembers(props.members),
staticMembers: GroupManager.staticMembers(props.members, props.keepInitialMembers, props.requiredMember),
contactFilter: '',
noContactsMessage: props.intl.formatMessage(messages.no_contacts),
selectedContacts: GroupManager.selectedContacts(props.members)
@@ -39,15 +41,25 @@ class GroupManager extends React.Component {
static indexMembers(members) {
let index = {};
members.map(function(m) {
members.map((m) => {
index[m.user] = {delta: 0, present: true}; // Delta: 0 unchanged, +1 added, -1 removed
});
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) {
let sel = [];
members.map(function(m) {
members.map((m) => {
sel.push(m.user);
});
return sel;
@@ -66,31 +78,31 @@ class GroupManager extends React.Component {
status = {delta: 1, present: true};
}
var m = this.state.members.slice();
let m = this.state.members.slice();
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;
this.setState({members: m, index: i, selectedContacts: sel});
}
handleMemberRemoved(userId, index) {
var status = this.state.index[userId];
const status = this.state.index[userId];
if (!status || !status.present) {
return;
}
status.present = false;
status.delta -= 1;
var m = this.state.members.slice();
let m = this.state.members.slice();
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;
this.setState({members: m, index: i, selectedContacts: sel});
@@ -107,7 +119,7 @@ class GroupManager extends React.Component {
static doContactFiltering(filter, values) {
if (filter) {
for (var i=0; i<values.length; i++) {
for (let i=0; i<values.length; i++) {
if (values[i].indexOf(filter) >= 0) {
return true;
}
@@ -154,7 +166,7 @@ class GroupManager extends React.Component {
<div className="panel-form-row">
<ChipInput
chips={this.state.members}
required={this.props.requiredMember}
staticMembers={this.state.staticMembers}
prompt="add members"
filterFunc={this.handleContactFilter}
onChipRemoved={this.handleMemberRemoved} />
+7 -4
View File
@@ -4,6 +4,8 @@ import { FormattedMessage } from 'react-intl';
import AvatarUpload from './avatar-upload.jsx';
import TagManager from './tag-manager.jsx';
import { MAX_TITLE_LENGTH } from '../config.js';
export default class NewTopicGroup extends React.PureComponent {
constructor(props) {
super(props);
@@ -41,10 +43,11 @@ export default class NewTopicGroup extends React.PureComponent {
handleSubmit(e) {
e.preventDefault();
if (this.state.fn && this.state.fn.trim()) {
this.props.onSubmit(this.state.fn.trim(),
this.state.imageDataUrl, this.state.private.trim(),
this.state.tags);
const fn = this.state.fn.trim().substring(0, MAX_TITLE_LENGTH);
const comment = this.state.private.trim().substring(0, MAX_TITLE_LENGTH);
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>
<br />
<div className="dialog-buttons">
<button className="outline" onClick={this.handleCancel}>
<FormattedMessage id="button_cancel" />
</button>
<button className="blue" onClick={this.handleSubmit}>
<FormattedMessage id="button_ok" />
</button>
<button className="white" onClick={this.handleCancel}>
<FormattedMessage id="button_cancel" />
</button>
</div>
</div>
);
+4 -4
View File
@@ -121,13 +121,13 @@ export default class TagManager extends React.Component {
filterFunc={this.handleTagInput} />
}</FormattedMessage>
{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}>
<FormattedMessage id="button_ok" defautMessage="OK" description="Confirmation button [OK]" />
</button>
<button className="white" onClick={this.handleCancel}>
<FormattedMessage id="button_cancel" defautMessage="Cancel" description="Rejection button [Cancel]" />
</button>
</div>
: null}
</div>
+720 -3036
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
+56 -41
View File
@@ -1,45 +1,60 @@
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const path = require('path');
module.exports = {
entry: [
path.resolve(__dirname, 'src/index.js'),
],
devtool: 'source-map',
module: {
rules: [
{
test: /\.jsx?$/,
loaders: [
'babel-loader',
],
exclude: /node_modules/,
},
],
},
optimization: {
minimizer: [
// we specify a custom UglifyJsPlugin here to get source maps in production.
new UglifyJsPlugin({
cache: true,
parallel: true,
uglifyOptions: {
compress: true,
mangle: true
module.exports = (env, argv) => {
const mode = argv.mode === 'production' ? 'prod' : 'dev';
return {
entry: {
index: path.resolve(__dirname, 'src/index.js'),
},
devtool: 'source-map',
module: {
rules: [
{
test: /\.jsx?$/,
loaders: [
'babel-loader',
],
exclude: /node_modules/,
},
sourceMap: true
})
]
},
performance: {
maxEntrypointSize: 280000,
maxAssetSize: 280000
},
externals: {
'react': 'React',
'react-dom': 'ReactDOM',
'react-intl': 'ReactIntl',
'firebase/app': 'firebase',
'firebase/messaging': ['firebase', 'messaging']
},
};
],
},
output: {
path: path.resolve(__dirname, 'umd'),
filename: `[name].${mode}.js`,
publicPath: '/umd/'
},
optimization: {
minimizer: [
// we specify a custom UglifyJsPlugin here to get source maps in production.
new UglifyJsPlugin({
cache: true,
parallel: true,
uglifyOptions: {
compress: true,
mangle: true
},
sourceMap: true
})
]
},
performance: {
maxEntrypointSize: 262144,
maxAssetSize: 262144
},
plugins: [
new CopyPlugin([
{ from: `node_modules/tinode-sdk/umd/tinode.${mode}.js`, to: `tinode.${mode}.js` },
]),
],
externals: {
'react': 'React',
'react-dom': 'ReactDOM',
'react-intl': 'ReactIntl',
'firebase/app': 'firebase',
'firebase/messaging': ['firebase', 'messaging'],
'tinode-sdk': 'Tinode',
},
};
}