Compare commits

..
37 Commits
Author SHA1 Message Date
or-else 79970cf558 getting rid of UNSAFE_componentWillReceiveProps 2019-02-24 19:30:11 +03:00
or-else 5d17ef7039 add new chat acceptance handlers 2019-02-24 15:14:02 +03:00
or-else 7e0aa5b93b more actions without subscribing first 2019-02-24 11:08:52 +03:00
or-else fd32413ba1 more work on confirming topic subscriptions, update permissions without subscribing 2019-02-21 22:05:40 +03:00
or-else 22b277a4ad add invitation.jsx widget 2019-02-18 19:10:16 +03:00
or-else da65e5663f added actions for chat invitation. bubble shadows 2019-02-18 19:09:39 +03:00
or-else 76d5ee1ed8 bump version and rebuild 2019-02-15 13:08:01 +03:00
or-else e7d68b5052 a possible fix for FF not focusing on a tab after notification click 2019-02-14 18:16:17 +03:00
or-else 1a17b01f0c add skipWaiting 2019-02-13 15:28:26 +03:00
or-else d7fcd8c761 bump version and rebuild 2019-02-11 16:13:11 +03:00
or-else 5e0cf3a2d2 0.15.13-rc1 2019-02-11 16:12:06 +03:00
or-else 636e38a6d7 rebuild 2019-02-11 16:09:45 +03:00
or-else 17d7e94138 change const -> let 2019-02-09 17:43:57 +03:00
or-else 49af44d4ca upkg.com is supposedly fixed now, rolling back the workaround 2019-02-09 10:53:46 +03:00
or-else 588b157005 update react dependencies to 16.8.1 2019-02-09 10:53:10 +03:00
or-else b5f926ef96 update dependencies due to a security issue in lodash 2019-02-09 10:50:22 +03:00
or-else 2e110a7a78 make sure the push token is sent to the server on app reload 2019-02-08 12:43:44 +03:00
or-else f710e5c76c mark generated css as binary to exclude from diff 2019-02-07 10:58:18 +03:00
or-else bedcc9a923 rebuild and mark generated .js and .js.map files as binary 2019-02-07 10:55:32 +03:00
or-else a554c0a9e1 minor fixes in chip-input 2019-02-06 20:20:51 +03:00
or-else ca2b3624e5 rebuild 2019-02-06 20:09:28 +03:00
or-else 87947b3de6 archiving works, refactored UNSAFE_componentWillReceiveProps in chips-input 2019-02-06 20:08:50 +03:00
or-else 160e2574d4 correct accoount editing for incremental updates 2019-02-04 13:05:03 +03:00
or-else a534387593 do inscremental updates instead of full updates 2019-02-04 12:46:17 +03:00
or-else 004dec20d3 automatic un-archiving on new message 2019-02-03 19:11:35 +03:00
or-else 68c0faf527 propagate {info} message to "me" topic 2019-02-03 18:29:59 +03:00
or-else 6bd64b2863 cleal localStorage on logout 2019-02-03 17:18:44 +03:00
or-else fc96d200ed archiving kind of works but buggy 2019-02-03 12:48:47 +03:00
or-else 7e30c2acc6 temporary fox for broken unpkg unpkg/unpkg.com#176 2019-02-03 09:48:52 +03:00
or-else 541ead2136 implementing chat archiving (does not work yet) 2019-02-02 21:31:58 +03:00
or-else 6388fedd70 deep-copy update private and public fields 2019-02-01 18:37:07 +03:00
or-else f34c342db3 rebuild 2019-02-01 12:14:22 +03:00
or-else 1883d35779 Merge branch 'devel' of https://github.com/tinode/webapp into archiving 2019-02-01 12:13:38 +03:00
or-else 15793cad26 add "archive" menu" 2019-02-01 11:50:22 +03:00
Gene 5e3d5a4be2 Merge pull request #28 from maksfreetimer/patch-2
service-worker: error on "fetch" event
2019-02-01 10:43:17 +03:00
Max 5f1c1f9f2e service-worker: error on "fetch" event
Chrome 70.0.3538.110
Sometimes i get this error: `Uncaught (in promise) TypeError: Failed to execute 'fetch' on 'ServiceWorkerGlobalScope': 'only-if-cached' can be set only with 'same-origin' mode
2019-02-01 10:32:39 +03:00
or-else a4af2a53ac pubm versions and rebuild 2019-01-29 14:22:42 +03:00
35 changed files with 3191 additions and 3719 deletions
+5
View File
@@ -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
View File
@@ -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 {
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -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
View File
@@ -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>
+1669 -2986
View File
File diff suppressed because it is too large Load Diff
+14 -12
View File
@@ -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
View File
@@ -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)
+7
View File
@@ -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
View File
@@ -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": "Заблокировать"
} }
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,3 +1,3 @@
// This is a generated file. Don't edit. // This is a generated file. Don't edit.
export const PACKAGE_VERSION = "0.15.12-rc3"; export const PACKAGE_VERSION = "0.15.13-rc3";
+41 -6
View File
@@ -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>
); );
} }
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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}
+9 -2
View File
@@ -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
View File
@@ -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 ?
-1
View File
@@ -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
View File
@@ -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}
+1 -1
View File
@@ -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 ?
+28
View File
@@ -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);
+25 -5
View File
@@ -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
View File
@@ -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');
} }
+42
View File
@@ -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 -6
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+5 -1
View File
@@ -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',