Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79970cf558 | ||
|
|
5d17ef7039 | ||
|
|
7e0aa5b93b | ||
|
|
fd32413ba1 | ||
|
|
22b277a4ad | ||
|
|
da65e5663f | ||
|
|
76d5ee1ed8 | ||
|
|
e7d68b5052 | ||
|
|
1a17b01f0c | ||
|
|
d7fcd8c761 | ||
|
|
5e0cf3a2d2 | ||
|
|
636e38a6d7 | ||
|
|
17d7e94138 | ||
|
|
49af44d4ca | ||
|
|
588b157005 | ||
|
|
b5f926ef96 | ||
|
|
2e110a7a78 | ||
|
|
f710e5c76c | ||
|
|
bedcc9a923 | ||
|
|
a554c0a9e1 | ||
|
|
ca2b3624e5 | ||
|
|
87947b3de6 | ||
|
|
160e2574d4 | ||
|
|
a534387593 | ||
|
|
004dec20d3 | ||
|
|
68c0faf527 | ||
|
|
6bd64b2863 | ||
|
|
fc96d200ed | ||
|
|
7e30c2acc6 | ||
|
|
541ead2136 | ||
|
|
6388fedd70 | ||
|
|
f34c342db3 | ||
|
|
1883d35779 | ||
|
|
15793cad26 | ||
|
|
5e3d5a4be2 | ||
|
|
5f1c1f9f2e | ||
|
|
a4af2a53ac |
@@ -0,0 +1,5 @@
|
|||||||
|
index.dev.js binary
|
||||||
|
index.dev.js.map binary
|
||||||
|
index.prod.js binary
|
||||||
|
index.prod.js.map binary
|
||||||
|
base.min.css binary
|
||||||
+56
-57
@@ -558,6 +558,7 @@ table.permission-editor td.checkbox {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-width: 8rem;
|
min-width: 8rem;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
|
padding: 0.35rem;
|
||||||
}
|
}
|
||||||
.bot-form div {
|
.bot-form div {
|
||||||
min-height: 2rem;
|
min-height: 2rem;
|
||||||
@@ -565,7 +566,7 @@ table.permission-editor td.checkbox {
|
|||||||
}
|
}
|
||||||
.bot-form button {
|
.bot-form button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0;
|
margin: 0.15rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #eef;
|
background-color: #eef;
|
||||||
color: #2196f3; /* blue 500 */
|
color: #2196f3; /* blue 500 */
|
||||||
@@ -727,6 +728,10 @@ i.search {
|
|||||||
.contact-box > li.selected {
|
.contact-box > li.selected {
|
||||||
background-color: #cfd8dc; /* blue-gray 100 highlight */
|
background-color: #cfd8dc; /* blue-gray 100 highlight */
|
||||||
}
|
}
|
||||||
|
.contact-box > li.action {
|
||||||
|
min-height: 2.5rem;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
.contact-box .text-box {
|
.contact-box .text-box {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
@@ -775,6 +780,14 @@ i.search {
|
|||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contact-box .action-text {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 2.5rem;
|
||||||
|
margin: 0 auto 0 auto;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/* Online-offline indicator positioned inside avatar-box */
|
/* Online-offline indicator positioned inside avatar-box */
|
||||||
.avatar-box .online, .offline {
|
.avatar-box .online, .offline {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -1074,7 +1087,7 @@ i.search {
|
|||||||
flex: none;
|
flex: none;
|
||||||
padding: 0.4rem 0.4rem 0.4rem 0.6rem;
|
padding: 0.4rem 0.4rem 0.4rem 0.6rem;
|
||||||
background-color: #eceff1;
|
background-color: #eceff1;
|
||||||
box-shadow: 0 -1px rgba(0,0,0,0.1); /* Faint shadow above panel */
|
box-shadow: 0px -2px 3px -1px rgba(0,0,0,0.1); /* Faint shadow above panel */
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
}
|
}
|
||||||
@@ -1096,6 +1109,38 @@ i.search {
|
|||||||
color: #2196f3!important;
|
color: #2196f3!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* New chat invitation panel */
|
||||||
|
|
||||||
|
.accept-invite-panel {
|
||||||
|
background-color: #eceff1;
|
||||||
|
box-shadow: 0px -2px 3px -1px rgba(0,0,0,0.1); /* Faint shadow above panel */
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accept-invite-panel .title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accept-invite-panel .footer {
|
||||||
|
background-color: #fafafa;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-bottom: 0.35rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accept-invite-panel button {
|
||||||
|
font-size: 1rem;
|
||||||
|
height: 2em;
|
||||||
|
line-height: 2em;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
margin: 0.25rem;
|
||||||
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* END of chat invitation panel */
|
||||||
|
|
||||||
/* A static panel shown when no topic is selected */
|
/* A static panel shown when no topic is selected */
|
||||||
#dummy-view {
|
#dummy-view {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -1188,6 +1233,7 @@ i.search {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-box li.single {
|
.chat-box li.single {
|
||||||
@@ -1225,15 +1271,16 @@ i.search {
|
|||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
right: auto;
|
right: auto;
|
||||||
left: -10px;
|
left: -.8rem;
|
||||||
top: auto;
|
top: auto;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
border: 10px solid;
|
border: 0.62rem solid #c5e1a5;
|
||||||
color: #c5e1a5;
|
color: #c5e1a5;
|
||||||
border-top-color: transparent;
|
border-top-color: transparent;
|
||||||
border-right-color: transparent;
|
border-right-color: inherit;
|
||||||
border-bottom-color: inherit;
|
border-bottom-color: inherit;
|
||||||
border-left-color: transparent;
|
border-left-color: transparent;
|
||||||
|
box-shadow: 0 1.6px 0.5px -0.8px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Right triangle placed bottom right flush. */
|
/* Right triangle placed bottom right flush. */
|
||||||
@@ -1245,15 +1292,16 @@ i.search {
|
|||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
left: auto;
|
left: auto;
|
||||||
right: -10px;
|
right: -.8rem;
|
||||||
top: auto;
|
top: auto;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
border: 10px solid;
|
border: 0.62rem solid #fafafa;
|
||||||
color: #fafafa;
|
color: #fafafa;
|
||||||
border-top-color: transparent;
|
border-top-color: transparent;
|
||||||
border-right-color: transparent;
|
border-right-color: transparent;
|
||||||
border-bottom-color: inherit;
|
border-bottom-color: inherit;
|
||||||
border-left-color: transparent;
|
border-left-color: inherit;
|
||||||
|
box-shadow: 0 1.6px 0.5px -0.8px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-box .avatar-box {
|
.chat-box .avatar-box {
|
||||||
@@ -1371,55 +1419,6 @@ i.search {
|
|||||||
|
|
||||||
/* END of InfoView */
|
/* END of InfoView */
|
||||||
|
|
||||||
/* Modal dialog */
|
|
||||||
.dialog-overlay {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 10;
|
|
||||||
background-color: rgba(255,255,255,0.90);
|
|
||||||
}
|
|
||||||
.modal-dialog {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
background-color: white;
|
|
||||||
min-width: 24rem;
|
|
||||||
min-height: 16rem;
|
|
||||||
margin: auto;
|
|
||||||
border-radius: 0.3rem;
|
|
||||||
box-shadow: 0 0.5rem 1rem 0 rgba(0,0,0, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-dialog .title-panel {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
.modal-dialog .body-panel {
|
|
||||||
margin: auto 0.5rem auto 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-dialog .dialog-buttons {
|
|
||||||
border-top: 1px solid #f5f5f5;
|
|
||||||
background-color: #fafafa
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-dialog .dialog-buttons a {
|
|
||||||
display: inline-block;
|
|
||||||
text-transform: uppercase;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
padding: 0.35rem 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* END of modal dialog */
|
|
||||||
|
|
||||||
/* BEGIN Tabs */
|
/* BEGIN Tabs */
|
||||||
|
|
||||||
ul.tabbar {
|
ul.tabbar {
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
@@ -30,8 +30,8 @@
|
|||||||
<script crossorigin="anonymous" src="https://unpkg.com/prop-types@15/prop-types.js"></script>
|
<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>
|
<script crossorigin="anonymous" src="https://unpkg.com/react-intl@2/dist/react-intl.js"></script>
|
||||||
<!-- Firebase scripts -->
|
<!-- Firebase scripts -->
|
||||||
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.5.6/firebase-app.js"></script>
|
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.8.1/firebase-app.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.5.6/firebase-messaging.js"></script>
|
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.8.1/firebase-messaging.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+5
-5
@@ -16,12 +16,12 @@
|
|||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,400italic,500,700" />
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,400italic,500,700" />
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
|
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
|
||||||
<link rel="stylesheet" href="css/base.min.css" />
|
<link rel="stylesheet" href="css/base.min.css" />
|
||||||
<script crossorigin="anonymous" src="https://unpkg.com/react@16.6.0/umd/react.production.min.js"></script>
|
<script crossorigin="anonymous" src="https://unpkg.com/react@16.8.1/umd/react.production.min.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.6.0/umd/react-dom.production.min.js"></script>
|
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.8.1/umd/react-dom.production.min.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://unpkg.com/prop-types@15.6.2/prop-types.min.js"></script>
|
<script crossorigin="anonymous" src="https://unpkg.com/prop-types@15.6.2/prop-types.min.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://unpkg.com/react-intl@2.7.2/dist/react-intl.min.js"></script>
|
<script crossorigin="anonymous" src="https://unpkg.com/react-intl@2.8.0/dist/react-intl.min.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.5.6/firebase-app.js"></script>
|
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.8.1/firebase-app.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.5.6/firebase-messaging.js"></script>
|
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.8.1/firebase-messaging.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript><div id="noscript-alertbox"><div id="noscript-title">Javascript support is required.</div>
|
<noscript><div id="noscript-alertbox"><div id="noscript-title">Javascript support is required.</div>
|
||||||
|
|||||||
Generated
+1669
-2986
File diff suppressed because it is too large
Load Diff
+14
-12
@@ -1,17 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "tinode-webapp",
|
"name": "tinode-webapp",
|
||||||
"description": "Tinode messenger for the web",
|
"description": "Tinode messenger for the web",
|
||||||
"version": "0.15.12-rc3",
|
"version": "0.15.13-rc3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.1.2",
|
"@babel/core": "^7.2.2",
|
||||||
"@babel/preset-env": "^7.1.0",
|
"@babel/preset-env": "^7.3.1",
|
||||||
"@babel/preset-react": "^7.0.0",
|
"@babel/preset-react": "^7.0.0",
|
||||||
"babel-loader": "^8.0.4",
|
"babel-loader": "^8.0.5",
|
||||||
"babel-plugin-react-intl": "^3.0.1",
|
"babel-plugin-react-intl": "^3.0.1",
|
||||||
"browserslist": "^4.4.1",
|
"browserslist": "^4.4.1",
|
||||||
"cssnano": "^4.0.5",
|
"cssnano": "^4.1.8",
|
||||||
"webpack": "^4.23.1",
|
"postcss": "^7.0.6",
|
||||||
"webpack-cli": "^3.1.2"
|
"uglifyjs-webpack-plugin": "^2.1.1",
|
||||||
|
"webpack": "^4.29.3",
|
||||||
|
"webpack-cli": "^3.2.3"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -75,10 +77,10 @@
|
|||||||
"author": "Tinode Authors <info@tinode.co>",
|
"author": "Tinode Authors <info@tinode.co>",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"firebase": "^5.5.5",
|
"firebase": "^5.8.1",
|
||||||
"react": "^16.5.2",
|
"react": "^16.8.1",
|
||||||
"react-dom": "^16.5.2",
|
"react-dom": "^16.8.1",
|
||||||
"react-intl": "^2.7.1",
|
"react-intl": "^2.8.0",
|
||||||
"tinode-sdk": "^0.15.11-rc2"
|
"tinode-sdk": "^0.15.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-21
@@ -1,6 +1,6 @@
|
|||||||
// Must be located at the root.
|
// Must be located at the root.
|
||||||
importScripts('https://unpkg.com/firebase@5.5.6/firebase-app.js');
|
importScripts('https://unpkg.com/firebase@5.8.1/firebase-app.js');
|
||||||
importScripts('https://unpkg.com/firebase@5.5.6/firebase-messaging.js');
|
importScripts('https://unpkg.com/firebase@5.8.1/firebase-messaging.js');
|
||||||
importScripts('firebase-init.js');
|
importScripts('firebase-init.js');
|
||||||
|
|
||||||
firebase.initializeApp(FIREBASE_INIT);
|
firebase.initializeApp(FIREBASE_INIT);
|
||||||
@@ -20,55 +20,59 @@ firebase.messaging().setBackgroundMessageHandler(function(payload) {
|
|||||||
return self.registration.showNotification(title, options);
|
return self.registration.showNotification(title, options);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update service worker immediately for both the current client
|
||||||
|
// and all other active clients.
|
||||||
|
self.addEventListener('install', event => {
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
// This code handles a click on notification: takes
|
// This code handles a click on notification: takes
|
||||||
// the user to the browser tab with the chat or opens a new tab.
|
// the user to the browser tab with the chat or opens a new tab.
|
||||||
self.addEventListener('notificationclick', function(event) {
|
self.addEventListener('notificationclick', event => {
|
||||||
const notification = event.notification;
|
const notification = event.notification;
|
||||||
notification.close();
|
notification.close();
|
||||||
|
|
||||||
const urlHash = '#/' + notification.data.topic;
|
const urlHash = '#/' + notification.data.topic;
|
||||||
|
|
||||||
const promises = self.clients.matchAll({
|
event.waitUntil(self.clients.matchAll({
|
||||||
type: 'window',
|
type: 'window',
|
||||||
includeUncontrolled: true
|
includeUncontrolled: true
|
||||||
}).then((windowClients) => {
|
}).then((windowClients) => {
|
||||||
let matchingClient = null;
|
|
||||||
let anyClient = null;
|
let anyClient = null;
|
||||||
for (let i = 0; i < windowClients.length; i++) {
|
for (let i = 0; i < windowClients.length; i++) {
|
||||||
const url = new URL(windowClients[i].url);
|
const url = new URL(windowClients[i].url);
|
||||||
if (url.hash.includes(notification.data.topic)) {
|
if (url.hash.includes(notification.data.topic)) {
|
||||||
matchingClient = windowClients[i];
|
// Found the Tinode tab with the right topic open.
|
||||||
break;
|
return windowClients[i].focus();
|
||||||
} else {
|
} else {
|
||||||
|
// This will be the least recently used tab.
|
||||||
anyClient = windowClients[i];
|
anyClient = windowClients[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Found browser tab with the topic already open.
|
// Found tab with Tinode on a different topic,
|
||||||
if (matchingClient) {
|
|
||||||
return matchingClient.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Found browser tab with Tinode on a different topic,
|
|
||||||
// navigate to the right topic.
|
// navigate to the right topic.
|
||||||
if (anyClient) {
|
if (anyClient) {
|
||||||
const url = new URL(anyClient.url);
|
const url = new URL(anyClient.url);
|
||||||
url.hash = '#/' + notification.data.topic;
|
url.hash = urlHash;
|
||||||
return anyClient.navigate(url).then(() => { return anyClient.focus(); });
|
return anyClient.focus().then(thisClient => {
|
||||||
|
return thisClient.navigate(url);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Did not find a Tinode browser tab. Open one.
|
// Did not find a Tinode browser tab. Open one.
|
||||||
const url = new URL(self.location.origin);
|
const url = new URL(self.location.origin);
|
||||||
url.hash = '#/' + notification.data.topic;
|
url.hash = urlHash;
|
||||||
return clients.openWindow(url);
|
return clients.openWindow(url);
|
||||||
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
event.waitUntil(promises);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// This is needed for 'Add to Home Screen'.
|
// This is needed for 'Add to Home Screen'.
|
||||||
self.addEventListener('fetch', function(event) {
|
self.addEventListener('fetch', event => {
|
||||||
|
// Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=823392
|
||||||
|
if (event.request.cache == 'only-if-cached' && event.request.mode != 'same-origin') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
// Try to find response in cache.
|
// Try to find response in cache.
|
||||||
caches.match(event.request)
|
caches.match(event.request)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"archived_contacts": "Archived contacts ({count})",
|
||||||
"contacts_not_found": "You have no chats<br />¯∖_(ツ)_/¯",
|
"contacts_not_found": "You have no chats<br />¯∖_(ツ)_/¯",
|
||||||
"full_name_prompt": "Full name, e.g. John Doe",
|
"full_name_prompt": "Full name, e.g. John Doe",
|
||||||
"email_prompt": "Email, e.g. jdoe@example.com",
|
"email_prompt": "Email, e.g. jdoe@example.com",
|
||||||
@@ -63,6 +64,7 @@
|
|||||||
"sidepanel_title_newtpk": "Start New Chat",
|
"sidepanel_title_newtpk": "Start New Chat",
|
||||||
"sidepanel_title_cred": "Confirm Credentials",
|
"sidepanel_title_cred": "Confirm Credentials",
|
||||||
"sidepanel_title_reset": "Reset Password",
|
"sidepanel_title_reset": "Reset Password",
|
||||||
|
"sidepanel_title_archive": "Archived Chats",
|
||||||
"update_available": "Update available. <a href=\"\">Reload</a>.",
|
"update_available": "Update available. <a href=\"\">Reload</a>.",
|
||||||
"reconnect_countdown": "Disconnected. Reconnecting in {seconds}s…",
|
"reconnect_countdown": "Disconnected. Reconnecting in {seconds}s…",
|
||||||
"reconnect_now": "Try now",
|
"reconnect_now": "Try now",
|
||||||
@@ -87,6 +89,7 @@
|
|||||||
"menu_item_unblock": "Unblock",
|
"menu_item_unblock": "Unblock",
|
||||||
"menu_item_block": "Block",
|
"menu_item_block": "Block",
|
||||||
"menu_item_member_delete": "Remove",
|
"menu_item_member_delete": "Remove",
|
||||||
|
"menu_item_archive_topic": "Archive",
|
||||||
"action_cancel": "cancel",
|
"action_cancel": "cancel",
|
||||||
"upload_finishing": "finishing...",
|
"upload_finishing": "finishing...",
|
||||||
"no_contacts": "You have no contacts :-(",
|
"no_contacts": "You have no contacts :-(",
|
||||||
@@ -99,6 +102,10 @@
|
|||||||
"label_file_name": "File name:",
|
"label_file_name": "File name:",
|
||||||
"label_content_type": "Content type:",
|
"label_content_type": "Content type:",
|
||||||
"label_size": "Size:",
|
"label_size": "Size:",
|
||||||
|
"chat_invitation": "You are invited to start a new chat. What would you like to do?",
|
||||||
|
"chat_invitation_accept": "Accept",
|
||||||
|
"chat_invitation_ignore": "Ignore",
|
||||||
|
"chat_invitation_block": "Block",
|
||||||
"error_invalid_id": "Invalid ID",
|
"error_invalid_id": "Invalid ID",
|
||||||
"group_user_id_prompt": "Group or User ID",
|
"group_user_id_prompt": "Group or User ID",
|
||||||
"button_subscribe": "Subscribe",
|
"button_subscribe": "Subscribe",
|
||||||
|
|||||||
+8
-1
@@ -122,5 +122,12 @@
|
|||||||
"user_not_found": "Не найден",
|
"user_not_found": "Не найден",
|
||||||
"reconnect_countdown": "Нет связи. Подключение через {seconds}с…",
|
"reconnect_countdown": "Нет связи. Подключение через {seconds}с…",
|
||||||
"reconnect_now": "Подключить сейчас.",
|
"reconnect_now": "Подключить сейчас.",
|
||||||
"save_attachment": "сохранить"
|
"save_attachment": "сохранить",
|
||||||
|
"menu_item_archive_topic": "В архив",
|
||||||
|
"archived_contacts": "Чаты в архиве ({count})",
|
||||||
|
"sidepanel_title_archive": "Архив чатов",
|
||||||
|
"chat_invitation": "Вас пригласили начать новый чат. Как вы хотите поступить?",
|
||||||
|
"chat_invitation_accept": "Принять",
|
||||||
|
"chat_invitation_ignore": "Игнорировать",
|
||||||
|
"chat_invitation_block": "Заблокировать"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ export default class LocalStorageUtil {
|
|||||||
|
|
||||||
// Get stored object.
|
// Get stored object.
|
||||||
static getObject(key) {
|
static getObject(key) {
|
||||||
let value = localStorage.getItem(key);
|
const value = localStorage.getItem(key);
|
||||||
return value && JSON.parse(value);
|
return value && JSON.parse(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Partially or wholly update stored object.
|
// Partially or wholly update stored object.
|
||||||
static updateObject(key, value) {
|
static updateObject(key, value) {
|
||||||
let oldVal = this.getObject(key);
|
const oldVal = this.getObject(key);
|
||||||
this.setObject(key, Object.assign(oldVal || {}, value));
|
this.setObject(key, Object.assign(oldVal || {}, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-8
@@ -2,23 +2,24 @@
|
|||||||
|
|
||||||
// Make shortcut icon appear with a green dot + show unread count in title.
|
// Make shortcut icon appear with a green dot + show unread count in title.
|
||||||
export function updateFavicon(count) {
|
export function updateFavicon(count) {
|
||||||
var oldIcon = document.getElementById('shortcut-icon');
|
const oldIcon = document.getElementById('shortcut-icon');
|
||||||
if (oldIcon) {
|
const head = document.head || document.getElementsByTagName('head')[0];
|
||||||
var head = document.head || document.getElementsByTagName('head')[0];
|
const newIcon = document.createElement('link');
|
||||||
var newIcon = document.createElement('link');
|
|
||||||
newIcon.type = 'image/png';
|
newIcon.type = 'image/png';
|
||||||
newIcon.id = 'shortcut-icon';
|
newIcon.id = 'shortcut-icon';
|
||||||
newIcon.rel = 'shortcut icon';
|
newIcon.rel = 'shortcut icon';
|
||||||
newIcon.href = 'img/logo32x32' + (count > 0 ? 'a' : '') + '.png';
|
newIcon.href = 'img/logo32x32' + (count > 0 ? 'a' : '') + '.png';
|
||||||
|
if (oldIcon) {
|
||||||
head.removeChild(oldIcon);
|
head.removeChild(oldIcon);
|
||||||
head.appendChild(newIcon);
|
|
||||||
}
|
}
|
||||||
|
head.appendChild(newIcon);
|
||||||
|
|
||||||
document.title = (count > 0 ? '('+count+') ' : '') + 'Tinode';
|
document.title = (count > 0 ? '('+count+') ' : '') + 'Tinode';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create VCard which represents topic 'public' info
|
// Create VCard which represents topic 'public' info
|
||||||
export function vcard(fn, imageDataUrl) {
|
export function vcard(fn, imageDataUrl) {
|
||||||
var card = null;
|
let card = null;
|
||||||
|
|
||||||
if ((fn && fn.trim()) || imageDataUrl) {
|
if ((fn && fn.trim()) || imageDataUrl) {
|
||||||
card = {};
|
card = {};
|
||||||
@@ -26,7 +27,7 @@ export function vcard(fn, imageDataUrl) {
|
|||||||
card.fn = fn.trim();
|
card.fn = fn.trim();
|
||||||
}
|
}
|
||||||
if (imageDataUrl) {
|
if (imageDataUrl) {
|
||||||
var dataStart = imageDataUrl.indexOf(',');
|
const dataStart = imageDataUrl.indexOf(',');
|
||||||
card.photo = {
|
card.photo = {
|
||||||
data: imageDataUrl.substring(dataStart+1),
|
data: imageDataUrl.substring(dataStart+1),
|
||||||
type: 'jpg'
|
type: 'jpg'
|
||||||
@@ -38,6 +39,14 @@ export function vcard(fn, imageDataUrl) {
|
|||||||
|
|
||||||
// Deep-shallow compare two arrays: shallow compare each element.
|
// Deep-shallow compare two arrays: shallow compare each element.
|
||||||
export function arrayEqual(a, b) {
|
export function arrayEqual(a, b) {
|
||||||
|
if (a === b) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(a) || !Array.isArray(b)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Compare lengths first.
|
// Compare lengths first.
|
||||||
if (a.length != b.length) {
|
if (a.length != b.length) {
|
||||||
return false;
|
return false;
|
||||||
@@ -45,7 +54,7 @@ export function arrayEqual(a, b) {
|
|||||||
// Order of elements is ignored.
|
// Order of elements is ignored.
|
||||||
a.sort();
|
a.sort();
|
||||||
b.sort();
|
b.sort();
|
||||||
for (var i = 0, l = a.length; i < l; i++) {
|
for (let i = 0, l = a.length; i < l; i++) {
|
||||||
if (a[i] !== b[i]) {
|
if (a[i] !== b[i]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-1
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"en": {
|
"en": {
|
||||||
|
"archived_contacts": "Archived contacts ({count})",
|
||||||
"contacts_not_found": "You have no chats<br />¯∖_(ツ)_/¯",
|
"contacts_not_found": "You have no chats<br />¯∖_(ツ)_/¯",
|
||||||
"full_name_prompt": "Full name, e.g. John Doe",
|
"full_name_prompt": "Full name, e.g. John Doe",
|
||||||
"email_prompt": "Email, e.g. jdoe@example.com",
|
"email_prompt": "Email, e.g. jdoe@example.com",
|
||||||
@@ -64,6 +65,7 @@
|
|||||||
"sidepanel_title_newtpk": "Start New Chat",
|
"sidepanel_title_newtpk": "Start New Chat",
|
||||||
"sidepanel_title_cred": "Confirm Credentials",
|
"sidepanel_title_cred": "Confirm Credentials",
|
||||||
"sidepanel_title_reset": "Reset Password",
|
"sidepanel_title_reset": "Reset Password",
|
||||||
|
"sidepanel_title_archive": "Archived Chats",
|
||||||
"update_available": "Update available. <a href=\"\">Reload</a>.",
|
"update_available": "Update available. <a href=\"\">Reload</a>.",
|
||||||
"reconnect_countdown": "Disconnected. Reconnecting in {seconds}s…",
|
"reconnect_countdown": "Disconnected. Reconnecting in {seconds}s…",
|
||||||
"reconnect_now": "Try now",
|
"reconnect_now": "Try now",
|
||||||
@@ -88,6 +90,7 @@
|
|||||||
"menu_item_unblock": "Unblock",
|
"menu_item_unblock": "Unblock",
|
||||||
"menu_item_block": "Block",
|
"menu_item_block": "Block",
|
||||||
"menu_item_member_delete": "Remove",
|
"menu_item_member_delete": "Remove",
|
||||||
|
"menu_item_archive_topic": "Archive",
|
||||||
"action_cancel": "cancel",
|
"action_cancel": "cancel",
|
||||||
"upload_finishing": "finishing...",
|
"upload_finishing": "finishing...",
|
||||||
"no_contacts": "You have no contacts :-(",
|
"no_contacts": "You have no contacts :-(",
|
||||||
@@ -100,6 +103,10 @@
|
|||||||
"label_file_name": "File name:",
|
"label_file_name": "File name:",
|
||||||
"label_content_type": "Content type:",
|
"label_content_type": "Content type:",
|
||||||
"label_size": "Size:",
|
"label_size": "Size:",
|
||||||
|
"chat_invitation": "You are invited to start a new chat. What would you like to do?",
|
||||||
|
"chat_invitation_accept": "Accept",
|
||||||
|
"chat_invitation_ignore": "Ignore",
|
||||||
|
"chat_invitation_block": "Block",
|
||||||
"error_invalid_id": "Invalid ID",
|
"error_invalid_id": "Invalid ID",
|
||||||
"group_user_id_prompt": "Group or User ID",
|
"group_user_id_prompt": "Group or User ID",
|
||||||
"button_subscribe": "Subscribe",
|
"button_subscribe": "Subscribe",
|
||||||
@@ -249,6 +256,13 @@
|
|||||||
"user_not_found": "Не найден",
|
"user_not_found": "Не найден",
|
||||||
"reconnect_countdown": "Нет связи. Подключение через {seconds}с…",
|
"reconnect_countdown": "Нет связи. Подключение через {seconds}с…",
|
||||||
"reconnect_now": "Подключить сейчас.",
|
"reconnect_now": "Подключить сейчас.",
|
||||||
"save_attachment": "сохранить"
|
"save_attachment": "сохранить",
|
||||||
|
"menu_item_archive_topic": "В архив",
|
||||||
|
"archived_contacts": "Чаты в архиве ({count})",
|
||||||
|
"sidepanel_title_archive": "Архив чатов",
|
||||||
|
"chat_invitation": "Вас пригласили начать новый чат. Как вы хотите поступить?",
|
||||||
|
"chat_invitation_accept": "Принять",
|
||||||
|
"chat_invitation_ignore": "Игнорировать",
|
||||||
|
"chat_invitation_block": "Заблокировать"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
// This is a generated file. Don't edit.
|
// This is a generated file. Don't edit.
|
||||||
|
|
||||||
export const PACKAGE_VERSION = "0.15.12-rc3";
|
export const PACKAGE_VERSION = "0.15.13-rc3";
|
||||||
|
|||||||
@@ -1,38 +1,72 @@
|
|||||||
/* ContactsView holds all contacts-related stuff */
|
/* ContactsView holds all contacts-related stuff */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedHTMLMessage } from 'react-intl';
|
import { FormattedHTMLMessage, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import ContactList from '../widgets/contact-list.jsx';
|
import ContactList from '../widgets/contact-list.jsx';
|
||||||
|
|
||||||
import { updateFavicon } from '../lib/utils.js';
|
import { updateFavicon } from '../lib/utils.js';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
archived_contacts_title: {
|
||||||
|
id: "archived_contacts",
|
||||||
|
defaultMessage: "Archived contacts ({count})",
|
||||||
|
description: "Label for archived chats"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default class ContactsView extends React.Component {
|
export default class ContactsView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this.handleAction = this.handleAction.bind(this);
|
||||||
|
|
||||||
this.state = ContactsView.getDerivedStateFromProps(props, {});
|
this.state = ContactsView.getDerivedStateFromProps(props, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(nextProps, prevState) {
|
static getDerivedStateFromProps(nextProps, prevState) {
|
||||||
let contacts = [];
|
const contacts = [];
|
||||||
let unreadThreads = 0;
|
let unreadThreads = 0;
|
||||||
|
let archivedCount = 0;
|
||||||
nextProps.chatList.map((c) => {
|
nextProps.chatList.map((c) => {
|
||||||
unreadThreads += c.unread > 0 ? 1 : 0;
|
if (c.private && c.private.arch) {
|
||||||
|
if (nextProps.archive) {
|
||||||
contacts.push(c);
|
contacts.push(c);
|
||||||
|
} else {
|
||||||
|
archivedCount ++;
|
||||||
|
}
|
||||||
|
} else if (!nextProps.archive) {
|
||||||
|
contacts.push(c);
|
||||||
|
unreadThreads += c.unread > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
contacts.sort(function(a, b) {
|
contacts.sort(function(a, b) {
|
||||||
return (b.touched || 0) - (a.touched || 0);
|
return (b.touched || 0) - (a.touched || 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (archivedCount > 0) {
|
||||||
|
contacts.push({
|
||||||
|
action: 'archive',
|
||||||
|
title: messages.archived_contacts_title,
|
||||||
|
values: {count: archivedCount}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
updateFavicon(unreadThreads);
|
updateFavicon(unreadThreads);
|
||||||
|
|
||||||
return {contactList: contacts};
|
return {
|
||||||
|
contactList: contacts,
|
||||||
|
archivedCount: archivedCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAction(action_ignored) {
|
||||||
|
this.props.onShowArchive();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<FormattedHTMLMessage id="contacts_not_found"
|
<FormattedHTMLMessage id="contacts_not_found"
|
||||||
defaultMesage="You have no chats<br />¯∖_(ツ)_/¯"
|
defaultMessage="You have no chats<br />¯∖_(ツ)_/¯"
|
||||||
description="HTML message shown in ContactList when no contacts are found">{
|
description="HTML message shown in ContactList when no contacts are found">{
|
||||||
(no_contacts) => <ContactList
|
(no_contacts) => <ContactList
|
||||||
connected={this.props.connected}
|
connected={this.props.connected}
|
||||||
@@ -43,7 +77,8 @@ export default class ContactsView extends React.Component {
|
|||||||
showOnline={true}
|
showOnline={true}
|
||||||
showUnread={true}
|
showUnread={true}
|
||||||
onTopicSelected={this.props.onTopicSelected}
|
onTopicSelected={this.props.onTopicSelected}
|
||||||
showContextMenu={this.props.showContextMenu} />
|
showContextMenu={this.props.showContextMenu}
|
||||||
|
onAction={this.handleAction} />
|
||||||
}</FormattedHTMLMessage>
|
}</FormattedHTMLMessage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export default class EditAccountView extends React.Component {
|
|||||||
|
|
||||||
handleFullNameUpdate(fn) {
|
handleFullNameUpdate(fn) {
|
||||||
this.setState({fullName: fn});
|
this.setState({fullName: fn});
|
||||||
this.props.onUpdateAccount(undefined, vcard(fn, this.state.avatar));
|
this.props.onUpdateAccount(undefined, vcard(fn, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePasswordUpdate(pwd) {
|
handlePasswordUpdate(pwd) {
|
||||||
@@ -70,7 +70,7 @@ export default class EditAccountView extends React.Component {
|
|||||||
|
|
||||||
handleImageChanged(img) {
|
handleImageChanged(img) {
|
||||||
this.setState({avatar: img});
|
this.setState({avatar: img});
|
||||||
this.props.onUpdateAccount(undefined, vcard(this.state.fullName, img));
|
this.props.onUpdateAccount(undefined, vcard(null, img));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCheckboxClick(what, checked) {
|
handleCheckboxClick(what, checked) {
|
||||||
|
|||||||
+7
-13
@@ -98,7 +98,7 @@ class InfoView extends React.Component {
|
|||||||
|
|
||||||
// No need to separately handle component mount.
|
// No need to separately handle component mount.
|
||||||
componentDidUpdate(props) {
|
componentDidUpdate(props) {
|
||||||
let topic = this.props.tinode.getTopic(props.topic);
|
const topic = this.props.tinode.getTopic(props.topic);
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -126,7 +126,7 @@ class InfoView extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
var topic = this.props.tinode.getTopic(this.props.topic);
|
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -185,7 +185,7 @@ class InfoView extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMetaDesc(desc) {
|
onMetaDesc(desc) {
|
||||||
var topic = this.props.tinode.getTopic(this.props.topic);
|
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -197,7 +197,7 @@ class InfoView extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSubsUpdated() {
|
onSubsUpdated() {
|
||||||
var topic = this.props.tinode.getTopic(this.props.topic);
|
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -220,26 +220,20 @@ class InfoView extends React.Component {
|
|||||||
fn = fn.trim();
|
fn = fn.trim();
|
||||||
if (this.state.fullName !== fn) {
|
if (this.state.fullName !== fn) {
|
||||||
this.setState({fullName: fn});
|
this.setState({fullName: fn});
|
||||||
this.props.onTopicDescUpdate(this.props.topic,
|
this.props.onTopicDescUpdate(this.props.topic, vcard(fn, null), null);
|
||||||
vcard(fn, this.state.avatar),
|
|
||||||
null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePrivateUpdate(priv) {
|
handlePrivateUpdate(priv) {
|
||||||
if (this.state.priv !== priv) {
|
if (this.state.priv !== priv) {
|
||||||
this.setState({private: priv});
|
this.setState({private: priv});
|
||||||
this.props.onTopicDescUpdate(this.props.topic,
|
this.props.onTopicDescUpdate(this.props.topic, null, priv || Tinode.DEL_CHAR);
|
||||||
null,
|
|
||||||
priv || Tinode.DEL_CHAR);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleImageChanged(img) {
|
handleImageChanged(img) {
|
||||||
this.setState({avatar: img});
|
this.setState({avatar: img});
|
||||||
this.props.onTopicDescUpdate(this.props.topic,
|
this.props.onTopicDescUpdate(this.props.topic, vcard(null, img), null);
|
||||||
vcard(this.state.fullName, img),
|
|
||||||
null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMuted(ignored, checked) {
|
handleMuted(ignored, checked) {
|
||||||
|
|||||||
+147
-104
@@ -8,6 +8,7 @@ import ChatMessage from '../widgets/chat-message.jsx';
|
|||||||
import ErrorPanel from '../widgets/error-panel.jsx';
|
import ErrorPanel from '../widgets/error-panel.jsx';
|
||||||
import GroupSubs from '../widgets/group-subs.jsx';
|
import GroupSubs from '../widgets/group-subs.jsx';
|
||||||
import ImagePreview from '../widgets/image-preview.jsx';
|
import ImagePreview from '../widgets/image-preview.jsx';
|
||||||
|
import Invitation from '../widgets/Invitation.jsx';
|
||||||
import LetterTile from '../widgets/letter-tile.jsx';
|
import LetterTile from '../widgets/letter-tile.jsx';
|
||||||
import LoadSpinner from '../widgets/load-spinner.jsx';
|
import LoadSpinner from '../widgets/load-spinner.jsx';
|
||||||
import LogoView from './logo-view.jsx';
|
import LogoView from './logo-view.jsx';
|
||||||
@@ -35,24 +36,19 @@ const messages = defineMessages({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Checks if the access permissions are granted but not yet accepted.
|
||||||
|
function isUnconfirmed(acs) {
|
||||||
|
const ex = acs ? acs.getExcessive() || '' : '';
|
||||||
|
return (ex.includes('R') || ex.includes('W'));
|
||||||
|
}
|
||||||
|
|
||||||
class MessagesView extends React.Component {
|
class MessagesView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = MessagesView.getDerivedStateFromProps(props, {});
|
||||||
messages: [],
|
|
||||||
onlineSubs: [],
|
|
||||||
topic: '',
|
|
||||||
title: '',
|
|
||||||
avatar: null,
|
|
||||||
scrollPosition: 0,
|
|
||||||
readOnly: props.acs ? !props.acs.isWriter() : true,
|
|
||||||
writeOnly: props.acs ? !props.acs.isReader() : true,
|
|
||||||
typingIndicator: false,
|
|
||||||
imagePreview: null
|
|
||||||
};
|
|
||||||
|
|
||||||
this.propsChange = this.propsChange.bind(this);
|
// this.propsChange = this.propsChange.bind(this);
|
||||||
this.leave = this.leave.bind(this);
|
this.leave = this.leave.bind(this);
|
||||||
this.handleScrollReference = this.handleScrollReference.bind(this);
|
this.handleScrollReference = this.handleScrollReference.bind(this);
|
||||||
this.handleScrollEvent = this.handleScrollEvent.bind(this);
|
this.handleScrollEvent = this.handleScrollEvent.bind(this);
|
||||||
@@ -67,22 +63,11 @@ class MessagesView extends React.Component {
|
|||||||
this.handleContextClick = this.handleContextClick.bind(this);
|
this.handleContextClick = this.handleContextClick.bind(this);
|
||||||
this.handleShowContextMenuMessage = this.handleShowContextMenuMessage.bind(this);
|
this.handleShowContextMenuMessage = this.handleShowContextMenuMessage.bind(this);
|
||||||
this.handleBackNavigation = this.handleBackNavigation.bind(this);
|
this.handleBackNavigation = this.handleBackNavigation.bind(this);
|
||||||
}
|
this.handleNewChatAcceptance = this.handleNewChatAcceptance.bind(this);
|
||||||
|
|
||||||
// Scroll last message into view on component update e.g. on message received
|
|
||||||
// or vertical shrinking.
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
if (this.messagesScroller) {
|
|
||||||
if (prevState.title != this.state.title || prevState.messages.length != this.state.messages.length) {
|
|
||||||
this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight - this.state.scrollPosition;
|
|
||||||
} else if (prevProps.viewportHeight > this.props.viewportHeight) {
|
|
||||||
this.messagesScroller.scrollTop += prevProps.viewportHeight - this.props.viewportHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.propsChange(this.props);
|
// this.propsChange(this.props, this.state);
|
||||||
if (this.messagesScroller) {
|
if (this.messagesScroller) {
|
||||||
this.messagesScroller.addEventListener('scroll', this.handleScrollEvent);
|
this.messagesScroller.addEventListener('scroll', this.handleScrollEvent);
|
||||||
}
|
}
|
||||||
@@ -95,92 +80,53 @@ class MessagesView extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
// Scroll last message into view on component update e.g. on message received
|
||||||
this.propsChange(nextProps);
|
// or vertical shrinking.
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
if (this.messagesScroller) {
|
||||||
|
if (prevState.topic != this.state.topic || prevState.messages.length != this.state.messages.length) {
|
||||||
|
this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight - this.state.scrollPosition;
|
||||||
|
} else if (prevProps.viewportHeight > this.props.viewportHeight) {
|
||||||
|
this.messagesScroller.scrollTop += prevProps.viewportHeight - this.props.viewportHeight;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
propsChange(props) {
|
const topic = this.props.tinode.getTopic(this.state.topic);
|
||||||
if (!props || !props.topic) {
|
if (this.state.topic != prevState.topic) {
|
||||||
let oldTopic = this.state.topic;
|
if (prevState.topic) {
|
||||||
this.setState({messages: [], onlineSubs: [], topic: null}, this.leave(oldTopic));
|
this.leave(prevState.topic);
|
||||||
return;
|
this.props.readTimerHandler(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.connected) {
|
if (topic) {
|
||||||
// connection lost, clear online subs
|
|
||||||
this.setState({onlineSubs: []});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let topic = this.props.tinode.getTopic(props.topic);
|
|
||||||
if (!topic) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let newGroupTopic = Tinode.isNewGroupTopicName(props.topic);
|
|
||||||
let tryToResubscribe = !this.props.connected && props.connected;
|
|
||||||
|
|
||||||
if (props.topic != this.state.topic) {
|
|
||||||
let msgs = [];
|
|
||||||
let subs = [];
|
|
||||||
|
|
||||||
// Bind the new topic to component.
|
|
||||||
topic.onData = this.handleNewMessage;
|
topic.onData = this.handleNewMessage;
|
||||||
topic.onAllMessagesReceived = this.handleAllMessagesReceived;
|
topic.onAllMessagesReceived = this.handleAllMessagesReceived;
|
||||||
topic.onInfo = this.handleInfoReceipt;
|
topic.onInfo = this.handleInfoReceipt;
|
||||||
topic.onMetaDesc = this.handleDescChange;
|
topic.onMetaDesc = this.handleDescChange;
|
||||||
topic.onSubsUpdated = this.handleSubsUpdated;
|
topic.onSubsUpdated = this.handleSubsUpdated;
|
||||||
topic.onPres = this.handleSubsUpdated;
|
topic.onPres = this.handleSubsUpdated;
|
||||||
// Unbind the previous topic from this component.
|
|
||||||
this.leave(this.state.topic);
|
|
||||||
|
|
||||||
this.handleDescChange(topic);
|
|
||||||
topic.subscribers((sub) => {
|
|
||||||
if (sub.online && sub.user != this.props.myUserId) {
|
|
||||||
subs.push(sub);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
topic.messages(function(msg) {
|
|
||||||
if (!msg.deleted) {
|
|
||||||
msgs = msgs.concat(msg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
messages: msgs,
|
|
||||||
onlineSubs: subs,
|
|
||||||
topic: props.topic,
|
|
||||||
imagePreview: null,
|
|
||||||
scrollPosition: 0
|
|
||||||
});
|
|
||||||
tryToResubscribe = true;
|
|
||||||
|
|
||||||
// The user switched to the new topic before the timer for
|
|
||||||
// the previous topic has triggered, kill it.
|
|
||||||
this.props.readTimerHandler(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
if (topic && !topic.isSubscribed() &&
|
||||||
readOnly: props.acs ? !props.acs.isWriter() : true,
|
((!prevProps.connected && this.props.connected) || (this.state.topic != prevState.topic))) {
|
||||||
writeOnly: props.acs ? !props.acs.isReader() : true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!topic.isSubscribed() && tryToResubscribe) {
|
|
||||||
// Don't request the tags. They are useless unless the user
|
// Don't request the tags. They are useless unless the user
|
||||||
// is the owner and is editing the topic.
|
// is the owner and is editing the topic.
|
||||||
let getQuery = topic.startMetaQuery()
|
const getQuery = topic.startMetaQuery()
|
||||||
.withLaterDesc()
|
.withLaterDesc()
|
||||||
.withLaterSub()
|
.withLaterSub()
|
||||||
.withLaterData(MESSAGES_PAGE)
|
.withLaterData(MESSAGES_PAGE)
|
||||||
.withLaterDel();
|
.withLaterDel();
|
||||||
let setQuery = newGroupTopic ? props.newGroupTopicParams : undefined;
|
const setQuery = Tinode.isNewGroupTopicName(this.props.topic) ?
|
||||||
|
this.props.newGroupTopicParams : undefined;
|
||||||
// Show "loading" spinner.
|
// Show "loading" spinner.
|
||||||
this.setState({ fetchingMessages: true });
|
this.setState({ fetchingMessages: true });
|
||||||
topic.subscribe(getQuery.build(), setQuery)
|
topic.subscribe(getQuery.build(), setQuery)
|
||||||
.then((ctrl) => {
|
.then((ctrl) => {
|
||||||
|
if (this.state.topic != ctrl.topic) {
|
||||||
this.setState({topic: ctrl.topic});
|
this.setState({topic: ctrl.topic});
|
||||||
this.props.onNewTopicCreated(props.topic, ctrl.topic);
|
}
|
||||||
|
this.props.onNewTopicCreated(this.props.topic, ctrl.topic);
|
||||||
// If there are unsent messages, try sending them now.
|
// If there are unsent messages, try sending them now.
|
||||||
topic.queuedMessages((pub) => {
|
topic.queuedMessages((pub) => {
|
||||||
if (!pub._sending && topic.isSubscribed()) {
|
if (!pub._sending && topic.isSubscribed()) {
|
||||||
@@ -189,23 +135,107 @@ class MessagesView extends React.Component {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
console.log('Failed subscription to', this.state.topic);
|
||||||
this.props.onError(err.message, 'err');
|
this.props.onError(err.message, 'err');
|
||||||
const {formatMessage} = this.props.intl;
|
const blankState = MessagesView.getDerivedStateFromProps({}, {});
|
||||||
this.setState({
|
blankState.title = this.props.intl.formatMessage(messages.not_found);
|
||||||
title: formatMessage(messages.not_found),
|
this.setState(blankState);
|
||||||
avatar: null,
|
|
||||||
readOnly: true,
|
|
||||||
writeOnly: true,
|
|
||||||
fetchingMessages: false,
|
|
||||||
messages: [],
|
|
||||||
onlineSubs: [],
|
|
||||||
typingIndicator: false,
|
|
||||||
imagePreview: null
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromProps(nextProps, prevState) {
|
||||||
|
let nextState = {};
|
||||||
|
if (!nextProps.topic) {
|
||||||
|
// Default state: no topic.
|
||||||
|
nextState = {
|
||||||
|
messages: [],
|
||||||
|
onlineSubs: [],
|
||||||
|
topic: null,
|
||||||
|
title: '',
|
||||||
|
avatar: null,
|
||||||
|
imagePreview: null,
|
||||||
|
typingIndicator: false,
|
||||||
|
scrollPosition: 0,
|
||||||
|
fetchingMessages: false
|
||||||
|
};
|
||||||
|
} else if (nextProps.topic != prevState.topic) {
|
||||||
|
const topic = nextProps.tinode.getTopic(nextProps.topic);
|
||||||
|
nextState = {
|
||||||
|
topic: nextProps.topic,
|
||||||
|
imagePreview: null,
|
||||||
|
typingIndicator: false,
|
||||||
|
scrollPosition: 0,
|
||||||
|
fetchingMessages: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (topic) {
|
||||||
|
// Topic exists.
|
||||||
|
const msgs = [];
|
||||||
|
const subs = [];
|
||||||
|
|
||||||
|
if (nextProps.connected) {
|
||||||
|
topic.subscribers((sub) => {
|
||||||
|
if (sub.online && sub.user != nextProps.myUserId) {
|
||||||
|
subs.push(sub);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
topic.messages(function(msg) {
|
||||||
|
if (!msg.deleted) {
|
||||||
|
msgs.push(msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
nextState = Object.assign(nextState, {
|
||||||
|
messages: msgs,
|
||||||
|
onlineSubs: subs
|
||||||
|
});
|
||||||
|
|
||||||
|
if (topic.public) {
|
||||||
|
nextState = Object.assign(nextState, {
|
||||||
|
title: topic.public.fn,
|
||||||
|
avatar: makeImageUrl(topic.public.photo)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
nextState = Object.assign(nextState, {
|
||||||
|
title: '',
|
||||||
|
avatar: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Invalid topic.
|
||||||
|
nextState = Object.assign(nextState, {
|
||||||
|
messages: [],
|
||||||
|
onlineSubs: [],
|
||||||
|
title: '',
|
||||||
|
avatar: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let val = nextProps.acs ? !nextProps.acs.isWriter() : true;
|
||||||
|
if (val != prevState.readOnly) {
|
||||||
|
nextState.readOnly = val;
|
||||||
|
}
|
||||||
|
val = nextProps.acs ? !nextProps.acs.isReader() : true;
|
||||||
|
if (val != prevState.writeOnly) {
|
||||||
|
nextState.writeOnly = val;
|
||||||
|
}
|
||||||
|
val = isUnconfirmed(nextProps.acs);
|
||||||
|
if (val != prevState.unconformed) {
|
||||||
|
nextState.unconfirmed = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear subscribers online when there is no connection.
|
||||||
|
if (!nextProps.connected && prevState.onlineSubs && prevState.onlineSubs.length > 0) {
|
||||||
|
nextState.onlineSubs = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextState;
|
||||||
|
}
|
||||||
|
|
||||||
leave(oldTopicName) {
|
leave(oldTopicName) {
|
||||||
if (!oldTopicName) {
|
if (!oldTopicName) {
|
||||||
return;
|
return;
|
||||||
@@ -268,15 +298,19 @@ class MessagesView extends React.Component {
|
|||||||
avatar: null
|
avatar: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (desc.acs) {
|
if (desc.acs) {
|
||||||
this.setState({
|
this.setState({
|
||||||
readOnly: !desc.acs.isWriter(),
|
readOnly: !desc.acs.isWriter(),
|
||||||
writeOnly: !desc.acs.isReader()
|
writeOnly: !desc.acs.isReader(),
|
||||||
|
unconfirmed: isUnconfirmed(desc.acs),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubsUpdated() {
|
handleSubsUpdated() {
|
||||||
|
console.log("handleSubsUpdated");
|
||||||
|
|
||||||
if (this.state.topic) {
|
if (this.state.topic) {
|
||||||
var subs = [];
|
var subs = [];
|
||||||
var topic = this.props.tinode.getTopic(this.state.topic);
|
var topic = this.props.tinode.getTopic(this.state.topic);
|
||||||
@@ -387,10 +421,10 @@ class MessagesView extends React.Component {
|
|||||||
|
|
||||||
handleShowContextMenuMessage(params) {
|
handleShowContextMenuMessage(params) {
|
||||||
params.topicName = this.state.topic;
|
params.topicName = this.state.topic;
|
||||||
var menuItems = ['message_delete'];
|
const menuItems = ['message_delete'];
|
||||||
var topic = this.props.tinode.getTopic(params.topicName);
|
const topic = this.props.tinode.getTopic(params.topicName);
|
||||||
if (topic) {
|
if (topic) {
|
||||||
var acs = topic.getAccessMode();
|
const acs = topic.getAccessMode();
|
||||||
if (acs && acs.isDeleter()) {
|
if (acs && acs.isDeleter()) {
|
||||||
menuItems.push('message_delete_hard');
|
menuItems.push('message_delete_hard');
|
||||||
}
|
}
|
||||||
@@ -402,6 +436,10 @@ class MessagesView extends React.Component {
|
|||||||
this.props.onHideMessagesView();
|
this.props.onHideMessagesView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleNewChatAcceptance(action) {
|
||||||
|
this.props.onNewChat(this.state.topic, action);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {formatMessage} = this.props.intl;
|
const {formatMessage} = this.props.intl;
|
||||||
|
|
||||||
@@ -526,6 +564,7 @@ class MessagesView extends React.Component {
|
|||||||
<ul id="scroller" className={chatBoxClass}>
|
<ul id="scroller" className={chatBoxClass}>
|
||||||
{messageNodes}
|
{messageNodes}
|
||||||
</ul>
|
</ul>
|
||||||
|
<div>Typing notifications here.</div>
|
||||||
</div>
|
</div>
|
||||||
{this.state.writeOnly ?
|
{this.state.writeOnly ?
|
||||||
<div id="write-only-background">
|
<div id="write-only-background">
|
||||||
@@ -538,12 +577,16 @@ class MessagesView extends React.Component {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
{this.state.unconfirmed ?
|
||||||
|
<Invitation onAction={this.handleNewChatAcceptance} />
|
||||||
|
:
|
||||||
<SendMessage
|
<SendMessage
|
||||||
tinode={this.props.tinode}
|
tinode={this.props.tinode}
|
||||||
topic={this.props.topic}
|
topic={this.props.topic}
|
||||||
disabled={this.state.readOnly}
|
disabled={this.state.readOnly}
|
||||||
sendMessage={this.props.sendMessage}
|
sendMessage={this.props.sendMessage}
|
||||||
onError={this.props.onError} />
|
onError={this.props.onError} />
|
||||||
|
}
|
||||||
{this.state.imagePreview ?
|
{this.state.imagePreview ?
|
||||||
<ImagePreview content={this.state.imagePreview}
|
<ImagePreview content={this.state.imagePreview}
|
||||||
onClose={this.handleCloseImagePreview} /> : null}
|
onClose={this.handleCloseImagePreview} /> : null}
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ const messages = defineMessages({
|
|||||||
id: 'sidepanel_title_reset',
|
id: 'sidepanel_title_reset',
|
||||||
description: 'Sidepanel title for PasswordResetView.',
|
description: 'Sidepanel title for PasswordResetView.',
|
||||||
defaultMessage: 'Reset Password'
|
defaultMessage: 'Reset Password'
|
||||||
|
},
|
||||||
|
'archive': {
|
||||||
|
id: 'sidepanel_title_archive',
|
||||||
|
description: 'Sidepanel title for ContactsView-Archive.',
|
||||||
|
defaultMessage: 'Archived Chats'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -134,15 +139,17 @@ class SidepanelView extends React.Component {
|
|||||||
onCancel={this.props.onCancel}
|
onCancel={this.props.onCancel}
|
||||||
onError={this.props.onError} /> :
|
onError={this.props.onError} /> :
|
||||||
|
|
||||||
view === 'contacts' ?
|
(view === 'contacts' || view == 'archive') ?
|
||||||
<ContactsView
|
<ContactsView
|
||||||
tinode={this.props.tinode}
|
tinode={this.props.tinode}
|
||||||
myUserId={this.props.myUserId}
|
myUserId={this.props.myUserId}
|
||||||
connected={this.props.connected}
|
connected={this.props.connected}
|
||||||
topicSelected={this.props.topicSelected}
|
topicSelected={this.props.topicSelected}
|
||||||
|
archive={view == 'archive'}
|
||||||
chatList={this.props.chatList}
|
chatList={this.props.chatList}
|
||||||
showContextMenu={this.props.showContextMenu}
|
showContextMenu={this.props.showContextMenu}
|
||||||
onTopicSelected={this.props.onTopicSelected} /> :
|
onTopicSelected={this.props.onTopicSelected}
|
||||||
|
onShowArchive={this.props.onShowArchive} /> :
|
||||||
|
|
||||||
view === 'newtpk' ?
|
view === 'newtpk' ?
|
||||||
<NewTopicView
|
<NewTopicView
|
||||||
|
|||||||
+104
-27
@@ -74,12 +74,14 @@ class TinodeWeb extends React.Component {
|
|||||||
this.handleTopicSelected = this.handleTopicSelected.bind(this);
|
this.handleTopicSelected = this.handleTopicSelected.bind(this);
|
||||||
this.handleHideMessagesView = this.handleHideMessagesView.bind(this);
|
this.handleHideMessagesView = this.handleHideMessagesView.bind(this);
|
||||||
this.handleSendMessage = this.handleSendMessage.bind(this);
|
this.handleSendMessage = this.handleSendMessage.bind(this);
|
||||||
|
this.handleNewChatInvitation = this.handleNewChatInvitation.bind(this);
|
||||||
this.handleNewAccount = this.handleNewAccount.bind(this);
|
this.handleNewAccount = this.handleNewAccount.bind(this);
|
||||||
this.handleNewAccountRequest = this.handleNewAccountRequest.bind(this);
|
this.handleNewAccountRequest = this.handleNewAccountRequest.bind(this);
|
||||||
this.handleUpdateAccountRequest = this.handleUpdateAccountRequest.bind(this);
|
this.handleUpdateAccountRequest = this.handleUpdateAccountRequest.bind(this);
|
||||||
this.handleUpdateAccountTagsRequest = this.handleUpdateAccountTagsRequest.bind(this);
|
this.handleUpdateAccountTagsRequest = this.handleUpdateAccountTagsRequest.bind(this);
|
||||||
this.handleSettings = this.handleSettings.bind(this);
|
this.handleSettings = this.handleSettings.bind(this);
|
||||||
this.handleGlobalSettings = this.handleGlobalSettings.bind(this);
|
this.handleGlobalSettings = this.handleGlobalSettings.bind(this);
|
||||||
|
this.handleShowArchive = this.handleShowArchive.bind(this);
|
||||||
this.handleToggleMessageSounds = this.handleToggleMessageSounds.bind(this);
|
this.handleToggleMessageSounds = this.handleToggleMessageSounds.bind(this);
|
||||||
this.initDesktopAlerts = this.initDesktopAlerts.bind(this);
|
this.initDesktopAlerts = this.initDesktopAlerts.bind(this);
|
||||||
this.togglePushToken = this.togglePushToken.bind(this);
|
this.togglePushToken = this.togglePushToken.bind(this);
|
||||||
@@ -103,10 +105,11 @@ class TinodeWeb extends React.Component {
|
|||||||
this.handleValidateCredentialsRequest = this.handleValidateCredentialsRequest.bind(this);
|
this.handleValidateCredentialsRequest = this.handleValidateCredentialsRequest.bind(this);
|
||||||
this.handlePasswordResetRequest = this.handlePasswordResetRequest.bind(this);
|
this.handlePasswordResetRequest = this.handlePasswordResetRequest.bind(this);
|
||||||
this.handleResetPassword = this.handleResetPassword.bind(this);
|
this.handleResetPassword = this.handleResetPassword.bind(this);
|
||||||
|
this.handleContextMenuAction = this.handleContextMenuAction.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBlankState() {
|
getBlankState() {
|
||||||
let settings = LocalStorageUtil.getObject("settings") || {};
|
const settings = LocalStorageUtil.getObject('settings') || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connected: false,
|
connected: false,
|
||||||
@@ -119,7 +122,7 @@ class TinodeWeb extends React.Component {
|
|||||||
desktopAlertsEnabled: (isSecureConnection() || isLocalHost()) &&
|
desktopAlertsEnabled: (isSecureConnection() || isLocalHost()) &&
|
||||||
(typeof firebase != 'undefined') && (typeof navigator != 'undefined') &&
|
(typeof firebase != 'undefined') && (typeof navigator != 'undefined') &&
|
||||||
(typeof FIREBASE_INIT != 'undefined'),
|
(typeof FIREBASE_INIT != 'undefined'),
|
||||||
firebaseToken: LocalStorageUtil.getObject("firebase-token"),
|
firebaseToken: LocalStorageUtil.getObject('firebase-token'),
|
||||||
|
|
||||||
errorText: '',
|
errorText: '',
|
||||||
errorLevel: null,
|
errorLevel: null,
|
||||||
@@ -192,7 +195,7 @@ class TinodeWeb extends React.Component {
|
|||||||
if (!this.state.firebaseToken) {
|
if (!this.state.firebaseToken) {
|
||||||
this.togglePushToken(true);
|
this.togglePushToken(true);
|
||||||
} else {
|
} else {
|
||||||
this.tinode.setDeviceToken(this.state.firebaseToken);
|
this.tinode.setDeviceToken(this.state.firebaseToken, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
@@ -206,10 +209,10 @@ class TinodeWeb extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let token = LocalStorageUtil.getObject('keep-logged-in') ?
|
const token = LocalStorageUtil.getObject('keep-logged-in') ?
|
||||||
LocalStorageUtil.getObject('auth-token') : undefined;
|
LocalStorageUtil.getObject('auth-token') : undefined;
|
||||||
|
|
||||||
let parsedNav = HashNavigation.parseUrlHash(window.location.hash);
|
const parsedNav = HashNavigation.parseUrlHash(window.location.hash);
|
||||||
if (token) {
|
if (token) {
|
||||||
// When reading from storage, date is returned as string.
|
// When reading from storage, date is returned as string.
|
||||||
token.expires = new Date(token.expires);
|
token.expires = new Date(token.expires);
|
||||||
@@ -274,7 +277,7 @@ class TinodeWeb extends React.Component {
|
|||||||
var hash = HashNavigation.parseUrlHash(window.location.hash);
|
var hash = HashNavigation.parseUrlHash(window.location.hash);
|
||||||
if (hash.path && hash.path.length > 0) {
|
if (hash.path && hash.path.length > 0) {
|
||||||
// Left-side panel selector.
|
// Left-side panel selector.
|
||||||
if (['register','settings','edit','cred','reset','newtpk','contacts',''].includes(hash.path[0])) {
|
if (['register','settings','edit','cred','reset','newtpk','archive','contacts',''].includes(hash.path[0])) {
|
||||||
this.setState({sidePanelSelected: hash.path[0]});
|
this.setState({sidePanelSelected: hash.path[0]});
|
||||||
} else {
|
} else {
|
||||||
console.log("Unknown sidepanel view", hash.path[0]);
|
console.log("Unknown sidepanel view", hash.path[0]);
|
||||||
@@ -696,7 +699,7 @@ class TinodeWeb extends React.Component {
|
|||||||
// User is sending a message, either plain text or a drafty object, possibly
|
// User is sending a message, either plain text or a drafty object, possibly
|
||||||
// with attachments.
|
// with attachments.
|
||||||
handleSendMessage(msg, promise, uploader) {
|
handleSendMessage(msg, promise, uploader) {
|
||||||
let topic = this.tinode.getTopic(this.state.topicSelected);
|
const topic = this.tinode.getTopic(this.state.topicSelected);
|
||||||
|
|
||||||
msg = topic.createMessage(msg, false);
|
msg = topic.createMessage(msg, false);
|
||||||
// The uploader is used to show progress.
|
// The uploader is used to show progress.
|
||||||
@@ -716,11 +719,51 @@ class TinodeWeb extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
topic.publishDraft(msg, promise)
|
topic.publishDraft(msg, promise)
|
||||||
|
.then((ctrl) => {
|
||||||
|
if (topic.isArchived()) {
|
||||||
|
return topic.archive(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
this.handleError(err.message, 'err');
|
this.handleError(err.message, 'err');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleNewChatInvitation(topicName, action) {
|
||||||
|
const topic = this.tinode.getTopic(topicName);
|
||||||
|
let response = null;
|
||||||
|
switch (action) {
|
||||||
|
case 'accept':
|
||||||
|
// Accept given permissions.
|
||||||
|
const mode = topic.getAccessMode().getGiven();
|
||||||
|
response = topic.setMeta({sub: {mode: mode}});
|
||||||
|
if (topic.getType() == 'p2p') {
|
||||||
|
// For P2P topics chage 'given' permission of the peer too.
|
||||||
|
// In p2p topics the other user has the same name as the topic.
|
||||||
|
response = response.then((ctrl) => {
|
||||||
|
topic.setMeta({sub: {user: topicName, mode: mode}});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
// Ignore invitation by deleting it.
|
||||||
|
response = topic.delTopic();
|
||||||
|
break;
|
||||||
|
case 'block':
|
||||||
|
// Ban the topic making futher invites impossible.
|
||||||
|
response = topic.setMeta({sub: {mode: 'N'}});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log("Unknown invitation action", '"' + action + '""');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response != null) {
|
||||||
|
response.catch((err) => {
|
||||||
|
this.handleError(err.message, 'err');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// User chose a Sign Up menu item.
|
// User chose a Sign Up menu item.
|
||||||
handleNewAccount() {
|
handleNewAccount() {
|
||||||
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, 'register'));
|
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, 'register'));
|
||||||
@@ -745,7 +788,14 @@ class TinodeWeb extends React.Component {
|
|||||||
|
|
||||||
handleUpdateAccountRequest(password, pub, defacs) {
|
handleUpdateAccountRequest(password, pub, defacs) {
|
||||||
if (pub || defacs) {
|
if (pub || defacs) {
|
||||||
this.tinode.getMeTopic().setMeta({desc: {public: pub, defacs: defacs}}).catch((err) => {
|
const params = {};
|
||||||
|
if (pub) {
|
||||||
|
params.public = pub;
|
||||||
|
}
|
||||||
|
if (defacs) {
|
||||||
|
params.defacs = defacs;
|
||||||
|
}
|
||||||
|
this.tinode.getMeTopic().setMeta({desc: params}).catch((err) => {
|
||||||
this.handleError(err.message, 'err');
|
this.handleError(err.message, 'err');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -793,12 +843,18 @@ class TinodeWeb extends React.Component {
|
|||||||
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, ''));
|
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User chose 'Archived chats'.
|
||||||
|
handleShowArchive() {
|
||||||
|
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash,
|
||||||
|
this.state.myUserId ? 'archive' : ''));
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize desktop alerts = push notifications.
|
// Initialize desktop alerts = push notifications.
|
||||||
initDesktopAlerts() {
|
initDesktopAlerts() {
|
||||||
// Google could not be bothered to mention that
|
// Google could not be bothered to mention that
|
||||||
// onTokenRefresh is never called.
|
// onTokenRefresh is never called.
|
||||||
this.fbPush.onTokenRefresh(() => {
|
this.fbPush.onTokenRefresh(() => {
|
||||||
this.requestPushToken(true);
|
this.requestPushToken();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.fbPush.onMessage((payload) => {
|
this.fbPush.onMessage((payload) => {
|
||||||
@@ -811,7 +867,7 @@ class TinodeWeb extends React.Component {
|
|||||||
if (enabled) {
|
if (enabled) {
|
||||||
if (!this.state.firebaseToken) {
|
if (!this.state.firebaseToken) {
|
||||||
this.fbPush.requestPermission().then(() => {
|
this.fbPush.requestPermission().then(() => {
|
||||||
this.requestPushToken(true);
|
this.requestPushToken();
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
this.handleError(err.message, 'err');
|
this.handleError(err.message, 'err');
|
||||||
this.setState({desktopAlerts: false, firebaseToken: null});
|
this.setState({desktopAlerts: false, firebaseToken: null});
|
||||||
@@ -836,10 +892,10 @@ class TinodeWeb extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
requestPushToken(sendToServer) {
|
requestPushToken() {
|
||||||
this.fbPush.getToken().then((refreshedToken) => {
|
this.fbPush.getToken().then((refreshedToken) => {
|
||||||
if (refreshedToken != this.state.firebaseToken) {
|
if (refreshedToken != this.state.firebaseToken) {
|
||||||
this.tinode.setDeviceToken(refreshedToken, sendToServer);
|
this.tinode.setDeviceToken(refreshedToken, true);
|
||||||
LocalStorageUtil.setObject('firebase-token', refreshedToken);
|
LocalStorageUtil.setObject('firebase-token', refreshedToken);
|
||||||
}
|
}
|
||||||
this.setState({firebaseToken: refreshedToken, desktopAlerts: true});
|
this.setState({firebaseToken: refreshedToken, desktopAlerts: true});
|
||||||
@@ -859,7 +915,7 @@ class TinodeWeb extends React.Component {
|
|||||||
|
|
||||||
// User clicked Cancel button in Setting or Sign Up panel.
|
// User clicked Cancel button in Setting or Sign Up panel.
|
||||||
handleSidepanelCancel() {
|
handleSidepanelCancel() {
|
||||||
var parsed = HashNavigation.parseUrlHash(window.location.hash);
|
const parsed = HashNavigation.parseUrlHash(window.location.hash);
|
||||||
parsed.path[0] = this.state.myUserId ? 'contacts' : '';
|
parsed.path[0] = this.state.myUserId ? 'contacts' : '';
|
||||||
if (parsed.params) {
|
if (parsed.params) {
|
||||||
delete parsed.params.code;
|
delete parsed.params.code;
|
||||||
@@ -877,7 +933,7 @@ class TinodeWeb extends React.Component {
|
|||||||
|
|
||||||
// Request to start a new topic. New P2P topic requires peer's name.
|
// Request to start a new topic. New P2P topic requires peer's name.
|
||||||
handleNewTopicRequest(peerName, pub, priv, tags) {
|
handleNewTopicRequest(peerName, pub, priv, tags) {
|
||||||
var topicName = peerName || this.tinode.newGroupTopicName();
|
const topicName = peerName || this.tinode.newGroupTopicName();
|
||||||
if (!peerName) {
|
if (!peerName) {
|
||||||
this.setState(
|
this.setState(
|
||||||
{newGroupTopicParams: {desc: {public: pub, private: {comment: priv}}, tags: tags}},
|
{newGroupTopicParams: {desc: {public: pub, private: {comment: priv}}, tags: tags}},
|
||||||
@@ -938,9 +994,8 @@ class TinodeWeb extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleTagsUpdated(topicName, tags) {
|
handleTagsUpdated(topicName, tags) {
|
||||||
var topic = this.tinode.getTopic(topicName);
|
const topic = this.tinode.getTopic(topicName);
|
||||||
if (topic) {
|
if (topic) {
|
||||||
var instance = this;
|
|
||||||
topic.setMeta({tags: tags}).catch((err) => {
|
topic.setMeta({tags: tags}).catch((err) => {
|
||||||
this.handleError(err.message, 'err');
|
this.handleError(err.message, 'err');
|
||||||
});
|
});
|
||||||
@@ -949,7 +1004,15 @@ class TinodeWeb extends React.Component {
|
|||||||
|
|
||||||
handleLogout() {
|
handleLogout() {
|
||||||
updateFavicon(0);
|
updateFavicon(0);
|
||||||
|
|
||||||
|
// Remove stored data.
|
||||||
localStorage.removeItem('auth-token');
|
localStorage.removeItem('auth-token');
|
||||||
|
localStorage.removeItem('firebase-token');
|
||||||
|
localStorage.removeItem('settings');
|
||||||
|
if (this.state.firebaseToken) {
|
||||||
|
this.fbPush.deleteToken(this.state.firebaseToken)
|
||||||
|
}
|
||||||
|
|
||||||
if (this.tinode) {
|
if (this.tinode) {
|
||||||
this.tinode.onDisconnect = undefined;
|
this.tinode.onDisconnect = undefined;
|
||||||
this.tinode.disconnect();
|
this.tinode.disconnect();
|
||||||
@@ -962,7 +1025,7 @@ class TinodeWeb extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleLeaveUnsubRequest(topicName) {
|
handleLeaveUnsubRequest(topicName) {
|
||||||
var topic = this.tinode.getTopic(topicName);
|
const topic = this.tinode.getTopic(topicName);
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -993,15 +1056,16 @@ class TinodeWeb extends React.Component {
|
|||||||
const topic = this.tinode.getTopic(topicName);
|
const topic = this.tinode.getTopic(topicName);
|
||||||
const {formatMessage} = this.props.intl;
|
const {formatMessage} = this.props.intl;
|
||||||
|
|
||||||
let muted = false, blocked = false, subscribed = false, deleter = false;
|
let muted = false, blocked = false, subscribed = false, deleter = false, archived = false;
|
||||||
if (topic) {
|
if (topic) {
|
||||||
if (topic.isSubscribed()) {
|
subscribed = topic.isSubscribed();
|
||||||
subscribed = true;
|
archived = topic.isArchived();
|
||||||
|
|
||||||
let acs = topic.getAccessMode();
|
const acs = topic.getAccessMode();
|
||||||
muted = acs && acs.isMuted();
|
if (acs) {
|
||||||
blocked = acs && !acs.isJoiner();
|
muted = acs.isMuted();
|
||||||
deleter = acs && acs.isDeleter();
|
blocked = !acs.isJoiner();
|
||||||
|
deleter = acs.isDeleter();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1009,8 +1073,9 @@ class TinodeWeb extends React.Component {
|
|||||||
subscribed ? {title: formatMessage({id: 'menu_item_info'}), handler: this.handleShowInfoView} : null,
|
subscribed ? {title: formatMessage({id: 'menu_item_info'}), handler: this.handleShowInfoView} : null,
|
||||||
subscribed ? 'messages_clear' : null,
|
subscribed ? 'messages_clear' : null,
|
||||||
subscribed && deleter ? 'messages_clear_hard' : null,
|
subscribed && deleter ? 'messages_clear_hard' : null,
|
||||||
subscribed ? (muted ? 'topic_unmute' : 'topic_mute') : null,
|
muted ? 'topic_unmute' : 'topic_mute',
|
||||||
subscribed ? (blocked ? 'topic_unblock' : 'topic_block') : null,
|
blocked ? 'topic_unblock' : 'topic_block',
|
||||||
|
!archived ? 'topic_archive' : null,
|
||||||
'topic_delete'
|
'topic_delete'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -1024,6 +1089,16 @@ class TinodeWeb extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleContextMenuAction(action, promise, params) {
|
||||||
|
if (action == 'topic_archive') {
|
||||||
|
if (promise && params.topicName && params.topicName == this.state.topicSelected) {
|
||||||
|
promise.then(() => {
|
||||||
|
this.handleTopicSelected(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleShowInfoView() {
|
handleShowInfoView() {
|
||||||
HashNavigation.navigateTo(HashNavigation.addUrlParam(window.location.hash, 'info', true));
|
HashNavigation.navigateTo(HashNavigation.addUrlParam(window.location.hash, 'info', true));
|
||||||
this.setState({showInfoPanel: true});
|
this.setState({showInfoPanel: true});
|
||||||
@@ -1039,7 +1114,7 @@ class TinodeWeb extends React.Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var topic = this.tinode.getTopic(topicName);
|
const topic = this.tinode.getTopic(topicName);
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1157,6 +1232,7 @@ class TinodeWeb extends React.Component {
|
|||||||
onValidateCredentials={this.handleValidateCredentialsRequest}
|
onValidateCredentials={this.handleValidateCredentialsRequest}
|
||||||
onPasswordResetRequest={this.handlePasswordResetRequest}
|
onPasswordResetRequest={this.handlePasswordResetRequest}
|
||||||
onResetPassword={this.handleResetPassword}
|
onResetPassword={this.handleResetPassword}
|
||||||
|
onShowArchive={this.handleShowArchive}
|
||||||
|
|
||||||
onInitFind={this.tnInitFind}
|
onInitFind={this.tnInitFind}
|
||||||
searchResults={this.state.searchResults}
|
searchResults={this.state.searchResults}
|
||||||
@@ -1192,6 +1268,7 @@ class TinodeWeb extends React.Component {
|
|||||||
onNewTopicCreated={this.handleNewTopicCreated}
|
onNewTopicCreated={this.handleNewTopicCreated}
|
||||||
readTimerHandler={this.handleReadTimer}
|
readTimerHandler={this.handleReadTimer}
|
||||||
showContextMenu={this.handleShowContextMenu}
|
showContextMenu={this.handleShowContextMenu}
|
||||||
|
onNewChat={this.handleNewChatInvitation}
|
||||||
sendMessage={this.handleSendMessage} />
|
sendMessage={this.handleSendMessage} />
|
||||||
|
|
||||||
{this.state.showInfoPanel ?
|
{this.state.showInfoPanel ?
|
||||||
|
|||||||
@@ -186,7 +186,6 @@ function draftyFormatter(style, data, values, key) {
|
|||||||
case 'FM':
|
case 'FM':
|
||||||
// Form
|
// Form
|
||||||
attr.className = 'bot-form';
|
attr.className = 'bot-form';
|
||||||
// console.log(data, attr);
|
|
||||||
break;
|
break;
|
||||||
case 'FE':
|
case 'FE':
|
||||||
// Form element formatting is dependent on element content.
|
// Form element formatting is dependent on element content.
|
||||||
|
|||||||
+20
-21
@@ -9,13 +9,7 @@ export default class ChipInput extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = ChipInput.getDerivedStateFromProps(props);
|
||||||
placeholder: props.chips ? '' : props.prompt,
|
|
||||||
sortedChips: ChipInput.sortChips(props.chips, props.required),
|
|
||||||
chipIndex: ChipInput.indexChips(props.chips),
|
|
||||||
input: '',
|
|
||||||
focused: false
|
|
||||||
};
|
|
||||||
|
|
||||||
this.handleTextInput = this.handleTextInput.bind(this);
|
this.handleTextInput = this.handleTextInput.bind(this);
|
||||||
this.removeChipAt = this.removeChipAt.bind(this);
|
this.removeChipAt = this.removeChipAt.bind(this);
|
||||||
@@ -25,21 +19,26 @@ export default class ChipInput extends React.Component {
|
|||||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
static getDerivedStateFromProps(nextProps, prevState) {
|
||||||
this.setState({
|
const state = {
|
||||||
|
placeholder: nextProps.chips ? '' : nextProps.prompt,
|
||||||
sortedChips: ChipInput.sortChips(nextProps.chips, nextProps.required),
|
sortedChips: ChipInput.sortChips(nextProps.chips, nextProps.required),
|
||||||
chipIndex: ChipInput.indexChips(nextProps.chips)
|
chipIndex: ChipInput.indexChips(nextProps.chips),
|
||||||
});
|
focused: prevState && prevState.focused
|
||||||
if (nextProps.chips.length > this.props.chips.length) {
|
};
|
||||||
|
|
||||||
|
if (!prevState || nextProps.chips.length > prevState.sortedChips.length) {
|
||||||
// Chip added: clear input.
|
// Chip added: clear input.
|
||||||
this.setState({input: ''});
|
state.input ='';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map chip index to user name
|
// Map chip index to user name
|
||||||
static indexChips(chips) {
|
static indexChips(chips) {
|
||||||
var index = {};
|
const index = {};
|
||||||
var count = 0;
|
let count = 0;
|
||||||
chips.map(function(item) {
|
chips.map(function(item) {
|
||||||
index[item.user] = count;
|
index[item.user] = count;
|
||||||
count ++;
|
count ++;
|
||||||
@@ -49,8 +48,8 @@ export default class ChipInput extends React.Component {
|
|||||||
|
|
||||||
// Have non-removable chips appear before all other chips.
|
// Have non-removable chips appear before all other chips.
|
||||||
static sortChips(chips, keep) {
|
static sortChips(chips, keep) {
|
||||||
var required = [];
|
const required = [];
|
||||||
var normal = [];
|
const normal = [];
|
||||||
chips.map(function(item) {
|
chips.map(function(item) {
|
||||||
if (item.user === keep) {
|
if (item.user === keep) {
|
||||||
required.push(item);
|
required.push(item);
|
||||||
@@ -69,7 +68,7 @@ export default class ChipInput extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeChipAt(idx) {
|
removeChipAt(idx) {
|
||||||
var removed = this.state.sortedChips[idx];
|
const removed = this.state.sortedChips[idx];
|
||||||
this.props.onChipRemoved(removed.user, this.state.chipIndex[removed.user]);
|
this.props.onChipRemoved(removed.user, this.state.chipIndex[removed.user]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,8 +107,8 @@ export default class ChipInput extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
var chips = [];
|
const chips = [];
|
||||||
var count = 0;
|
let count = 0;
|
||||||
this.state.sortedChips.map((item) => {
|
this.state.sortedChips.map((item) => {
|
||||||
chips.push(
|
chips.push(
|
||||||
<Chip
|
<Chip
|
||||||
@@ -124,7 +123,7 @@ export default class ChipInput extends React.Component {
|
|||||||
);
|
);
|
||||||
count++;
|
count++;
|
||||||
});
|
});
|
||||||
var className = "chip-input" + (this.state.focused ? ' focused' : '');
|
const className = "chip-input" + (this.state.focused ? ' focused' : '');
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{chips}
|
{chips}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default class Chip extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
var title = this.props.title || this.props.topic;
|
const title = this.props.title || this.props.topic;
|
||||||
return (
|
return (
|
||||||
<div className="chip">
|
<div className="chip">
|
||||||
{this.props.noAvatar ?
|
{this.props.noAvatar ?
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// A panel included into a list of contacts with an action text.
|
||||||
|
import React from 'react';
|
||||||
|
import { injectIntl } from 'react-intl';
|
||||||
|
|
||||||
|
class ContactAction extends React.PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.handleClick = this.handleClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.props.onAction(this.props.action);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {formatMessage} = this.props.intl;
|
||||||
|
return (
|
||||||
|
<li onClick={this.handleClick} className="action">
|
||||||
|
<div className="action-text">{formatMessage(this.props.title, this.props.values)}</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(ContactAction);
|
||||||
@@ -4,6 +4,7 @@ import React from 'react';
|
|||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import Contact from './contact.jsx';
|
import Contact from './contact.jsx';
|
||||||
|
import ContactAction from './contact-action.jsx';
|
||||||
|
|
||||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
import { makeImageUrl } from '../lib/blob-helpers.js';
|
||||||
|
|
||||||
@@ -25,9 +26,20 @@ class ContactList extends React.Component {
|
|||||||
const {formatMessage} = this.props.intl;
|
const {formatMessage} = this.props.intl;
|
||||||
const showCheckmark = Array.isArray(this.props.topicSelected);
|
const showCheckmark = Array.isArray(this.props.topicSelected);
|
||||||
const contactNodes = [];
|
const contactNodes = [];
|
||||||
|
let contactsCount = 0;
|
||||||
if (this.props.contacts && this.props.contacts.length > 0) {
|
if (this.props.contacts && this.props.contacts.length > 0) {
|
||||||
this.props.contacts.map((c) => {
|
this.props.contacts.map((c) => {
|
||||||
let key = c.topic ? c.topic : c.user;
|
if (c.action) {
|
||||||
|
// Action item
|
||||||
|
contactNodes.push(
|
||||||
|
<ContactAction
|
||||||
|
title={c.title} action={c.action} values={c.values}
|
||||||
|
key={c.action}
|
||||||
|
onAction={this.props.onAction}
|
||||||
|
/>);
|
||||||
|
} else {
|
||||||
|
// Normal contact
|
||||||
|
const key = c.topic ? c.topic : c.user;
|
||||||
// If filter function is provided, filter out the items
|
// If filter function is provided, filter out the items
|
||||||
// which don't satisfy the condition.
|
// which don't satisfy the condition.
|
||||||
if (this.props.filterFunc && this.props.filter) {
|
if (this.props.filterFunc && this.props.filter) {
|
||||||
@@ -43,10 +55,10 @@ class ContactList extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let selected = showCheckmark ?
|
const selected = showCheckmark ?
|
||||||
(this.props.topicSelected.indexOf(key) > -1) :
|
(this.props.topicSelected.indexOf(key) > -1) :
|
||||||
(this.props.topicSelected === key);
|
(this.props.topicSelected === key);
|
||||||
let badges = [];
|
const badges = [];
|
||||||
if (this.props.showMode) {
|
if (this.props.showMode) {
|
||||||
if (key === this.props.myUserId) {
|
if (key === this.props.myUserId) {
|
||||||
badges.push({name: formatMessage(messages.badge_you), color: 'green'});
|
badges.push({name: formatMessage(messages.badge_you), color: 'green'});
|
||||||
@@ -55,7 +67,7 @@ class ContactList extends React.Component {
|
|||||||
badges.push({name: formatMessage(messages.badge_owner), color: 'blue'});
|
badges.push({name: formatMessage(messages.badge_owner), color: 'blue'});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let comment = Array.isArray(c.private) ?
|
const comment = Array.isArray(c.private) ?
|
||||||
c.private.join(',') : (c.private ? c.private.comment : null);
|
c.private.join(',') : (c.private ? c.private.comment : null);
|
||||||
|
|
||||||
contactNodes.push(
|
contactNodes.push(
|
||||||
@@ -77,17 +89,25 @@ class ContactList extends React.Component {
|
|||||||
index={contactNodes.length}
|
index={contactNodes.length}
|
||||||
key={key} />
|
key={key} />
|
||||||
);
|
);
|
||||||
|
contactsCount ++;
|
||||||
|
}
|
||||||
}, this);
|
}, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={this.props.noScroll ? null : "scrollable-panel"}>
|
<div className={this.props.noScroll ? null : "scrollable-panel"}>
|
||||||
|
{contactsCount == 0 ?
|
||||||
|
<div className="center-medium-text"
|
||||||
|
dangerouslySetInnerHTML={{__html: this.props.emptyListMessage}} />
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
{contactNodes.length > 0 ?
|
{contactNodes.length > 0 ?
|
||||||
<ul className="contact-box">
|
<ul className="contact-box">
|
||||||
{contactNodes}
|
{contactNodes}
|
||||||
</ul>
|
</ul>
|
||||||
:
|
:
|
||||||
<div className="center-medium-text" dangerouslySetInnerHTML={{__html: this.props.emptyListMessage}} />
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+126
-34
@@ -60,6 +60,11 @@ const messages = defineMessages({
|
|||||||
id: 'menu_item_member_delete',
|
id: 'menu_item_member_delete',
|
||||||
defaultMessage: 'Remove',
|
defaultMessage: 'Remove',
|
||||||
description: 'Remove user from topic'
|
description: 'Remove user from topic'
|
||||||
|
},
|
||||||
|
archive: {
|
||||||
|
id: 'menu_item_archive_topic',
|
||||||
|
defaultMessage: 'Archive',
|
||||||
|
description: 'Move topic from the list of active chats to archive'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,54 +80,133 @@ class ContextMenu extends React.Component {
|
|||||||
|
|
||||||
// Preconfigured menu items.
|
// Preconfigured menu items.
|
||||||
this.MenuItems = {
|
this.MenuItems = {
|
||||||
'topic_info': {title: formatMessage(messages.info), handler: null},
|
'topic_info': {
|
||||||
|
id: 'topic_info',
|
||||||
'messages_clear': {title: formatMessage(messages.clear_messages), handler: (params, errorHandler) => {
|
title: formatMessage(messages.info),
|
||||||
this.deleteMessages(true, false, params, errorHandler);
|
handler: null
|
||||||
}},
|
},
|
||||||
'messages_clear_hard': {title: formatMessage(messages.clear_for_all), handler: (params, errorHandler) => {
|
'messages_clear': {
|
||||||
this.deleteMessages(true, true, params, errorHandler);
|
id: 'messages_clear',
|
||||||
}},
|
title: formatMessage(messages.clear_messages),
|
||||||
'message_delete': {title: formatMessage(messages.delete), handler: (params, errorHandler) => {
|
handler: (params, errorHandler) => {
|
||||||
this.deleteMessages(false, false, params, errorHandler);
|
return this.deleteMessages(true, false, params, errorHandler);
|
||||||
}},
|
}
|
||||||
'message_delete_hard': {title: formatMessage(messages.delete_for_all), handler: (params, errorHandler) => {
|
},
|
||||||
this.deleteMessages(false, true, params, errorHandler);
|
'messages_clear_hard': {
|
||||||
}},
|
id: 'messages_clear_hard',
|
||||||
'topic_unmute': {title: formatMessage(messages.unmute), handler: this.topicPermissionSetter.bind(this, '+P')},
|
title: formatMessage(messages.clear_for_all),
|
||||||
'topic_mute': {title: formatMessage(messages.mute), handler: this.topicPermissionSetter.bind(this, '-P')},
|
handler: (params, errorHandler) => {
|
||||||
'topic_unblock': {title: formatMessage(messages.unblock), handler: this.topicPermissionSetter.bind(this, '+J')},
|
return this.deleteMessages(true, true, params, errorHandler);
|
||||||
'topic_block': {title: formatMessage(messages.block), handler: this.topicPermissionSetter.bind(this, '-J')},
|
}
|
||||||
'topic_delete': {title: formatMessage(messages.topic_delete), handler: (params, errorHandler) => {
|
},
|
||||||
let topic = this.props.tinode.getTopic(params.topicName);
|
'message_delete': {
|
||||||
|
id: 'message_delete',
|
||||||
|
title: formatMessage(messages.delete),
|
||||||
|
handler: (params, errorHandler) => {
|
||||||
|
return this.deleteMessages(false, false, params, errorHandler);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'message_delete_hard': {
|
||||||
|
id: 'message_delete_hard',
|
||||||
|
title: formatMessage(messages.delete_for_all),
|
||||||
|
handler: (params, errorHandler) => {
|
||||||
|
return this.deleteMessages(false, true, params, errorHandler);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'topic_unmute': {
|
||||||
|
id: 'topic_unmute',
|
||||||
|
title: formatMessage(messages.unmute),
|
||||||
|
handler: this.topicPermissionSetter.bind(this, '+P')
|
||||||
|
},
|
||||||
|
'topic_mute': {
|
||||||
|
id: 'topic_mute',
|
||||||
|
title: formatMessage(messages.mute),
|
||||||
|
handler: this.topicPermissionSetter.bind(this, '-P')
|
||||||
|
},
|
||||||
|
'topic_unblock': {
|
||||||
|
id: 'topic_unblock',
|
||||||
|
title: formatMessage(messages.unblock),
|
||||||
|
handler: this.topicPermissionSetter.bind(this, '+J')
|
||||||
|
},
|
||||||
|
'topic_block': {
|
||||||
|
id: 'topic_block',
|
||||||
|
title: formatMessage(messages.block),
|
||||||
|
handler: this.topicPermissionSetter.bind(this, '-J')
|
||||||
|
},
|
||||||
|
'topic_delete': {
|
||||||
|
id: 'topic_delete',
|
||||||
|
title: formatMessage(messages.topic_delete),
|
||||||
|
handler: (params, errorHandler) => {
|
||||||
|
const topic = this.props.tinode.getTopic(params.topicName);
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
console.log("Topic not found: ", params.topicName);
|
console.log("Topic not found: ", params.topicName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
topic.delTopic().catch((err) => {
|
return topic.delTopic().catch((err) => {
|
||||||
if (errorHandler) {
|
if (errorHandler) {
|
||||||
errorHandler(err.message, 'err');
|
errorHandler(err.message, 'err');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}},
|
}
|
||||||
|
},
|
||||||
|
'topic_archive': {
|
||||||
|
id: 'topic_archive',
|
||||||
|
title: formatMessage(messages.archive),
|
||||||
|
handler: (params, errorHandler) => {
|
||||||
|
const topic = this.props.tinode.getTopic(params.topicName);
|
||||||
|
if (!topic) {
|
||||||
|
console.log("Topic not found: ", params.topicName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return topic.archive(true).catch((err) => {
|
||||||
|
if (errorHandler) {
|
||||||
|
errorHandler(err.message, 'err');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
// menu_item_edit_permissions is defined elsewhere.
|
// menu_item_edit_permissions is defined elsewhere.
|
||||||
'permissions': {title: formatMessage({id: 'menu_item_edit_permissions'}), handler: null},
|
'permissions': {
|
||||||
'member_delete': {title: formatMessage(messages.member_delete), handler: (params, errorHandler) => {
|
id: 'permissions',
|
||||||
let topic = this.props.tinode.getTopic(params.topicName);
|
title: formatMessage({id: 'menu_item_edit_permissions'}),
|
||||||
|
handler: null
|
||||||
|
},
|
||||||
|
'member_delete': {
|
||||||
|
id: 'member_delete',
|
||||||
|
title: formatMessage(messages.member_delete),
|
||||||
|
handler: (params, errorHandler) => {
|
||||||
|
const topic = this.props.tinode.getTopic(params.topicName);
|
||||||
if (!topic || !params.user) {
|
if (!topic || !params.user) {
|
||||||
console.log("Topic or user not found: '" + params.topicName + "', '" + params.user + "'");
|
console.log("Topic or user not found: '" + params.topicName + "', '" + params.user + "'");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
topic.delSubscription(params.user).catch((err) => {
|
return topic.delSubscription(params.user).catch((err) => {
|
||||||
if (errorHandler) {
|
if (errorHandler) {
|
||||||
errorHandler(err.message, 'err');
|
errorHandler(err.message, 'err');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}},
|
}
|
||||||
'member_mute': {title: formatMessage(messages.mute), handler: this.topicPermissionSetter.bind(this, '-P')},
|
},
|
||||||
'member_unmute': {title: formatMessage(messages.unmute), handler: this.topicPermissionSetter.bind(this, '+P')},
|
'member_mute': {
|
||||||
'member_block': {title: formatMessage(messages.block), handler: this.topicPermissionSetter.bind(this, '-J')},
|
id: 'member_mute',
|
||||||
'member_unblock': {title: formatMessage(messages.unblock), handler: this.topicPermissionSetter.bind(this, '+J')},
|
title: formatMessage(messages.mute),
|
||||||
|
handler: this.topicPermissionSetter.bind(this, '-P')
|
||||||
|
},
|
||||||
|
'member_unmute': {
|
||||||
|
id: 'member_unmute',
|
||||||
|
title: formatMessage(messages.unmute),
|
||||||
|
handler: this.topicPermissionSetter.bind(this, '+P')
|
||||||
|
},
|
||||||
|
'member_block': {
|
||||||
|
id: 'member_block',
|
||||||
|
title: formatMessage(messages.block),
|
||||||
|
handler: this.topicPermissionSetter.bind(this, '-J')
|
||||||
|
},
|
||||||
|
'member_unblock': {
|
||||||
|
id: 'member_unblock',
|
||||||
|
title: formatMessage(messages.unblock),
|
||||||
|
handler: this.topicPermissionSetter.bind(this, '+J')
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +243,15 @@ class ContextMenu extends React.Component {
|
|||||||
if (typeof item == 'string') {
|
if (typeof item == 'string') {
|
||||||
item = this.MenuItems[item];
|
item = this.MenuItems[item];
|
||||||
}
|
}
|
||||||
item.handler(this.props.params, this.props.onError);
|
|
||||||
|
if (!item) {
|
||||||
|
console.log("Invalid menu item ID", e.currentTarget.dataset.id);
|
||||||
|
} else {
|
||||||
|
this.props.onAction(
|
||||||
|
item.id,
|
||||||
|
item.handler(this.props.params, this.props.onError),
|
||||||
|
this.props.params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Menu Actions
|
// Menu Actions
|
||||||
@@ -183,7 +275,7 @@ class ContextMenu extends React.Component {
|
|||||||
topic.delMessagesAll(hard) :
|
topic.delMessagesAll(hard) :
|
||||||
topic.delMessagesList([params.seq], hard);
|
topic.delMessagesList([params.seq], hard);
|
||||||
|
|
||||||
promise.catch((err) => {
|
return promise.catch((err) => {
|
||||||
if (errorHandler) {
|
if (errorHandler) {
|
||||||
errorHandler(err.message, 'err');
|
errorHandler(err.message, 'err');
|
||||||
}
|
}
|
||||||
@@ -210,7 +302,7 @@ class ContextMenu extends React.Component {
|
|||||||
am = topic.getAccessMode().updateWant(mode).getWant();
|
am = topic.getAccessMode().updateWant(mode).getWant();
|
||||||
}
|
}
|
||||||
|
|
||||||
topic.setMeta({sub: {user: params.user, mode: am}}).catch((err) => {
|
return topic.setMeta({sub: {user: params.user, mode: am}}).catch((err) => {
|
||||||
if (errorHandler) {
|
if (errorHandler) {
|
||||||
errorHandler(err.message, 'err');
|
errorHandler(err.message, 'err');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// A single topic or user.
|
||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
export default class Invitation extends React.PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.handleButtonAction = this.handleButtonAction.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleButtonAction(evt, data) {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.props.onAction(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="accept-invite-panel">
|
||||||
|
<div className="title">
|
||||||
|
<FormattedMessage id="chat_invitation"
|
||||||
|
defaultMessage="You are invited to start a new chat. What would you like to do?"
|
||||||
|
description="New chat invitation message: [Accept] [Ignore] [Block]." />
|
||||||
|
</div>
|
||||||
|
<div className="footer">
|
||||||
|
<button className="blue" onClick={event => { this.handleButtonAction(event, "accept"); }}>
|
||||||
|
<FormattedMessage id="chat_invitation_accept"
|
||||||
|
defaultMessage="Accept" description="Action [Accept] for chat invitation." />
|
||||||
|
</button>
|
||||||
|
<button className="white" onClick={event => { this.handleButtonAction(event, "delete"); }}>
|
||||||
|
<FormattedMessage id="chat_invitation_ignore"
|
||||||
|
defaultMessage="Ignore" description="Action [Ignore] for chat invitation." />
|
||||||
|
</button>
|
||||||
|
<button className="white" onClick={event => { this.handleButtonAction(event, "block"); }}>
|
||||||
|
<FormattedMessage id="chat_invitation_block"
|
||||||
|
defaultMessage="Block" description="Action [Block] for chat invitation." />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -3,11 +3,8 @@ import React from 'react';
|
|||||||
|
|
||||||
export default class UnreadBadge extends React.PureComponent {
|
export default class UnreadBadge extends React.PureComponent {
|
||||||
render() {
|
render() {
|
||||||
var showUnreadBadge = null;
|
return (this.props.count > 0 ?
|
||||||
if (this.props.count > 0) {
|
<span className="unread">{this.props.count > 9 ? "9+" : this.props.count}</span>
|
||||||
var count = this.props.count > 9 ? "9+" : this.props.count;
|
: null);
|
||||||
showUnreadBadge = <span className="unread">{count}</span>;
|
|
||||||
}
|
|
||||||
return showUnreadBadge;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+647
-242
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
+5
-1
@@ -8,7 +8,7 @@ module.exports = {
|
|||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /src\/.*\.jsx?$/,
|
test: /src\/.*\.(js|jsx)?$/,
|
||||||
loaders: [
|
loaders: [
|
||||||
'babel-loader',
|
'babel-loader',
|
||||||
],
|
],
|
||||||
@@ -30,6 +30,10 @@ module.exports = {
|
|||||||
})
|
})
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
performance: {
|
||||||
|
maxEntrypointSize: 280000,
|
||||||
|
maxAssetSize: 280000
|
||||||
|
},
|
||||||
externals: {
|
externals: {
|
||||||
'react': 'React',
|
'react': 'React',
|
||||||
'react-dom': 'ReactDOM',
|
'react-dom': 'ReactDOM',
|
||||||
|
|||||||
Reference in New Issue
Block a user