Compare commits
41
Commits
12a0660e5b
...
6a2efac566
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a2efac566 | ||
|
|
8c1b926008 | ||
|
|
c539e5f70d | ||
|
|
154ec5000e | ||
|
|
80c5103ce0 | ||
|
|
15c0ff2e75 | ||
|
|
a404734794 | ||
|
|
f962c8e8a9 | ||
|
|
bb20937bc9 | ||
|
|
3026e21b46 | ||
|
|
92b9faa063 | ||
|
|
a1894871aa | ||
|
|
bd03a7164e | ||
|
|
9891620188 | ||
|
|
afa0726bdf | ||
|
|
1a927e5420 | ||
|
|
5f4db76cbf | ||
|
|
4092324fea | ||
|
|
efc7377942 | ||
|
|
94f88855de | ||
|
|
73afafb605 | ||
|
|
fe556a0707 | ||
|
|
79d0c6dd14 | ||
|
|
8e9349b58b | ||
|
|
22710adbe3 | ||
|
|
b8a1b39bba | ||
|
|
ef341cb94e | ||
|
|
cda001e5c6 | ||
|
|
254c4768eb | ||
|
|
8f0125587f | ||
|
|
1aaf6f8fab | ||
|
|
1d56d55487 | ||
|
|
205ca95217 | ||
|
|
f9a29ed3ef | ||
|
|
f3420dc66f | ||
|
|
6d581c8e7c | ||
|
|
10bb206d51 | ||
|
|
a2747b6e33 | ||
|
|
b3b0ae59ab | ||
|
|
d99434000f | ||
|
|
2a164ddf49 |
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
}]
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 46 KiB |
+12
-11
@@ -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
@@ -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>
|
||||
|
||||
Generated
+924
-606
File diff suppressed because it is too large
Load Diff
+17
-13
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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?"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
|
||||
@@ -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> <FormattedMessage id="button_logout"
|
||||
defaultMessage="Logout" description="Button [Logout]" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
+214
-92
@@ -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" /> <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)}:
|
||||
<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" /> <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)}:
|
||||
<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> {
|
||||
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> {formatMessage(messages.leave_chat)}
|
||||
</a>
|
||||
{!this.state.groupTopic ?
|
||||
<a href="#" className="red flat-button" onClick={this.handleBlock}>
|
||||
<i className="material-icons">block</i> {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> {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> <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>
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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} />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
<>
|
||||
 <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,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} />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6916
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+56
-41
@@ -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',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user