Compare commits
37
Commits
9bbddc08e6
...
79970cf558
| 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;
|
||||
min-width: 8rem;
|
||||
line-height: 2rem;
|
||||
padding: 0.35rem;
|
||||
}
|
||||
.bot-form div {
|
||||
min-height: 2rem;
|
||||
@@ -565,7 +566,7 @@ table.permission-editor td.checkbox {
|
||||
}
|
||||
.bot-form button {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
margin: 0.15rem;
|
||||
width: 100%;
|
||||
background-color: #eef;
|
||||
color: #2196f3; /* blue 500 */
|
||||
@@ -727,6 +728,10 @@ i.search {
|
||||
.contact-box > li.selected {
|
||||
background-color: #cfd8dc; /* blue-gray 100 highlight */
|
||||
}
|
||||
.contact-box > li.action {
|
||||
min-height: 2.5rem;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.contact-box .text-box {
|
||||
overflow-x: hidden;
|
||||
@@ -775,6 +780,14 @@ i.search {
|
||||
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 */
|
||||
.avatar-box .online, .offline {
|
||||
display: inline-block;
|
||||
@@ -1074,7 +1087,7 @@ i.search {
|
||||
flex: none;
|
||||
padding: 0.4rem 0.4rem 0.4rem 0.6rem;
|
||||
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;
|
||||
height: 4rem;
|
||||
}
|
||||
@@ -1096,6 +1109,38 @@ i.search {
|
||||
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 */
|
||||
#dummy-view {
|
||||
width: 100%;
|
||||
@@ -1188,6 +1233,7 @@ i.search {
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.chat-box li.single {
|
||||
@@ -1225,15 +1271,16 @@ i.search {
|
||||
width: 0;
|
||||
height: 0;
|
||||
right: auto;
|
||||
left: -10px;
|
||||
left: -.8rem;
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
border: 10px solid;
|
||||
border: 0.62rem solid #c5e1a5;
|
||||
color: #c5e1a5;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-right-color: inherit;
|
||||
border-bottom-color: inherit;
|
||||
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. */
|
||||
@@ -1245,15 +1292,16 @@ i.search {
|
||||
width: 0;
|
||||
height: 0;
|
||||
left: auto;
|
||||
right: -10px;
|
||||
right: -.8rem;
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
border: 10px solid;
|
||||
border: 0.62rem solid #fafafa;
|
||||
color: #fafafa;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
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 {
|
||||
@@ -1371,55 +1419,6 @@ i.search {
|
||||
|
||||
/* 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 */
|
||||
|
||||
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/react-intl@2/dist/react-intl.js"></script>
|
||||
<!-- 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.5.6/firebase-messaging.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.8.1/firebase-messaging.js"></script>
|
||||
</head>
|
||||
|
||||
<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/icon?family=Material+Icons" />
|
||||
<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-dom@16.6.0/umd/react-dom.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.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/react-intl@2.7.2/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.5.6/firebase-messaging.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.8.1/firebase-app.js"></script>
|
||||
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.8.1/firebase-messaging.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript><div id="noscript-alertbox"><div id="noscript-title">Javascript support is required.</div>
|
||||
|
||||
Generated
+1670
-2987
File diff suppressed because it is too large
Load Diff
+14
-12
@@ -1,17 +1,19 @@
|
||||
{
|
||||
"name": "tinode-webapp",
|
||||
"description": "Tinode messenger for the web",
|
||||
"version": "0.15.12-rc3",
|
||||
"version": "0.15.13-rc3",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.1.2",
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"@babel/core": "^7.2.2",
|
||||
"@babel/preset-env": "^7.3.1",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"babel-loader": "^8.0.4",
|
||||
"babel-loader": "^8.0.5",
|
||||
"babel-plugin-react-intl": "^3.0.1",
|
||||
"browserslist": "^4.4.1",
|
||||
"cssnano": "^4.0.5",
|
||||
"webpack": "^4.23.1",
|
||||
"webpack-cli": "^3.1.2"
|
||||
"cssnano": "^4.1.8",
|
||||
"postcss": "^7.0.6",
|
||||
"uglifyjs-webpack-plugin": "^2.1.1",
|
||||
"webpack": "^4.29.3",
|
||||
"webpack-cli": "^3.2.3"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -75,10 +77,10 @@
|
||||
"author": "Tinode Authors <info@tinode.co>",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"firebase": "^5.5.5",
|
||||
"react": "^16.5.2",
|
||||
"react-dom": "^16.5.2",
|
||||
"react-intl": "^2.7.1",
|
||||
"tinode-sdk": "^0.15.11-rc2"
|
||||
"firebase": "^5.8.1",
|
||||
"react": "^16.8.1",
|
||||
"react-dom": "^16.8.1",
|
||||
"react-intl": "^2.8.0",
|
||||
"tinode-sdk": "^0.15.12"
|
||||
}
|
||||
}
|
||||
|
||||
+26
-22
@@ -1,6 +1,6 @@
|
||||
// Must be located at the root.
|
||||
importScripts('https://unpkg.com/firebase@5.5.6/firebase-app.js');
|
||||
importScripts('https://unpkg.com/firebase@5.5.6/firebase-messaging.js');
|
||||
importScripts('https://unpkg.com/firebase@5.8.1/firebase-app.js');
|
||||
importScripts('https://unpkg.com/firebase@5.8.1/firebase-messaging.js');
|
||||
importScripts('firebase-init.js');
|
||||
|
||||
firebase.initializeApp(FIREBASE_INIT);
|
||||
@@ -20,56 +20,60 @@ firebase.messaging().setBackgroundMessageHandler(function(payload) {
|
||||
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
|
||||
// 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;
|
||||
notification.close();
|
||||
|
||||
const urlHash = '#/' + notification.data.topic;
|
||||
|
||||
const promises = self.clients.matchAll({
|
||||
event.waitUntil(self.clients.matchAll({
|
||||
type: 'window',
|
||||
includeUncontrolled: true
|
||||
}).then((windowClients) => {
|
||||
let matchingClient = null;
|
||||
let anyClient = null;
|
||||
for (let i = 0; i < windowClients.length; i++) {
|
||||
const url = new URL(windowClients[i].url);
|
||||
if (url.hash.includes(notification.data.topic)) {
|
||||
matchingClient = windowClients[i];
|
||||
break;
|
||||
// Found the Tinode tab with the right topic open.
|
||||
return windowClients[i].focus();
|
||||
} else {
|
||||
// This will be the least recently used tab.
|
||||
anyClient = windowClients[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Found browser tab with the topic already open.
|
||||
if (matchingClient) {
|
||||
return matchingClient.focus();
|
||||
}
|
||||
|
||||
// Found browser tab with Tinode on a different topic,
|
||||
// Found tab with Tinode on a different topic,
|
||||
// navigate to the right topic.
|
||||
if (anyClient) {
|
||||
const url = new URL(anyClient.url);
|
||||
url.hash = '#/' + notification.data.topic;
|
||||
return anyClient.navigate(url).then(() => { return anyClient.focus(); });
|
||||
url.hash = urlHash;
|
||||
return anyClient.focus().then(thisClient => {
|
||||
return thisClient.navigate(url);
|
||||
});
|
||||
}
|
||||
|
||||
// Did not find a Tinode browser tab. Open one.
|
||||
const url = new URL(self.location.origin);
|
||||
url.hash = '#/' + notification.data.topic;
|
||||
url.hash = urlHash;
|
||||
return clients.openWindow(url);
|
||||
|
||||
});
|
||||
|
||||
event.waitUntil(promises);
|
||||
}));
|
||||
});
|
||||
|
||||
// This is needed for 'Add to Home Screen'.
|
||||
self.addEventListener('fetch', function(event) {
|
||||
event.respondWith(
|
||||
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(
|
||||
// Try to find response in cache.
|
||||
caches.match(event.request)
|
||||
.then((resp) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"archived_contacts": "Archived contacts ({count})",
|
||||
"contacts_not_found": "You have no chats<br />¯∖_(ツ)_/¯",
|
||||
"full_name_prompt": "Full name, e.g. John Doe",
|
||||
"email_prompt": "Email, e.g. jdoe@example.com",
|
||||
@@ -63,6 +64,7 @@
|
||||
"sidepanel_title_newtpk": "Start New Chat",
|
||||
"sidepanel_title_cred": "Confirm Credentials",
|
||||
"sidepanel_title_reset": "Reset Password",
|
||||
"sidepanel_title_archive": "Archived Chats",
|
||||
"update_available": "Update available. <a href=\"\">Reload</a>.",
|
||||
"reconnect_countdown": "Disconnected. Reconnecting in {seconds}s…",
|
||||
"reconnect_now": "Try now",
|
||||
@@ -87,6 +89,7 @@
|
||||
"menu_item_unblock": "Unblock",
|
||||
"menu_item_block": "Block",
|
||||
"menu_item_member_delete": "Remove",
|
||||
"menu_item_archive_topic": "Archive",
|
||||
"action_cancel": "cancel",
|
||||
"upload_finishing": "finishing...",
|
||||
"no_contacts": "You have no contacts :-(",
|
||||
@@ -99,6 +102,10 @@
|
||||
"label_file_name": "File name:",
|
||||
"label_content_type": "Content type:",
|
||||
"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",
|
||||
"group_user_id_prompt": "Group or User ID",
|
||||
"button_subscribe": "Subscribe",
|
||||
|
||||
+8
-1
@@ -122,5 +122,12 @@
|
||||
"user_not_found": "Не найден",
|
||||
"reconnect_countdown": "Нет связи. Подключение через {seconds}с…",
|
||||
"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.
|
||||
static getObject(key) {
|
||||
let value = localStorage.getItem(key);
|
||||
const value = localStorage.getItem(key);
|
||||
return value && JSON.parse(value);
|
||||
}
|
||||
|
||||
// Partially or wholly update stored object.
|
||||
static updateObject(key, value) {
|
||||
let oldVal = this.getObject(key);
|
||||
const oldVal = this.getObject(key);
|
||||
this.setObject(key, Object.assign(oldVal || {}, value));
|
||||
}
|
||||
|
||||
|
||||
+20
-11
@@ -2,23 +2,24 @@
|
||||
|
||||
// Make shortcut icon appear with a green dot + show unread count in title.
|
||||
export function updateFavicon(count) {
|
||||
var oldIcon = document.getElementById('shortcut-icon');
|
||||
const oldIcon = document.getElementById('shortcut-icon');
|
||||
const head = document.head || document.getElementsByTagName('head')[0];
|
||||
const newIcon = document.createElement('link');
|
||||
newIcon.type = 'image/png';
|
||||
newIcon.id = 'shortcut-icon';
|
||||
newIcon.rel = 'shortcut icon';
|
||||
newIcon.href = 'img/logo32x32' + (count > 0 ? 'a' : '') + '.png';
|
||||
if (oldIcon) {
|
||||
var head = document.head || document.getElementsByTagName('head')[0];
|
||||
var newIcon = document.createElement('link');
|
||||
newIcon.type = 'image/png';
|
||||
newIcon.id = 'shortcut-icon';
|
||||
newIcon.rel = 'shortcut icon';
|
||||
newIcon.href = 'img/logo32x32' + (count > 0 ? 'a' : '') + '.png';
|
||||
head.removeChild(oldIcon);
|
||||
head.appendChild(newIcon);
|
||||
}
|
||||
head.appendChild(newIcon);
|
||||
|
||||
document.title = (count > 0 ? '('+count+') ' : '') + 'Tinode';
|
||||
}
|
||||
|
||||
// Create VCard which represents topic 'public' info
|
||||
export function vcard(fn, imageDataUrl) {
|
||||
var card = null;
|
||||
let card = null;
|
||||
|
||||
if ((fn && fn.trim()) || imageDataUrl) {
|
||||
card = {};
|
||||
@@ -26,7 +27,7 @@ export function vcard(fn, imageDataUrl) {
|
||||
card.fn = fn.trim();
|
||||
}
|
||||
if (imageDataUrl) {
|
||||
var dataStart = imageDataUrl.indexOf(',');
|
||||
const dataStart = imageDataUrl.indexOf(',');
|
||||
card.photo = {
|
||||
data: imageDataUrl.substring(dataStart+1),
|
||||
type: 'jpg'
|
||||
@@ -38,6 +39,14 @@ export function vcard(fn, imageDataUrl) {
|
||||
|
||||
// Deep-shallow compare two arrays: shallow compare each element.
|
||||
export function arrayEqual(a, b) {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Array.isArray(a) || !Array.isArray(b)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare lengths first.
|
||||
if (a.length != b.length) {
|
||||
return false;
|
||||
@@ -45,7 +54,7 @@ export function arrayEqual(a, b) {
|
||||
// Order of elements is ignored.
|
||||
a.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]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
+15
-1
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"en": {
|
||||
"archived_contacts": "Archived contacts ({count})",
|
||||
"contacts_not_found": "You have no chats<br />¯∖_(ツ)_/¯",
|
||||
"full_name_prompt": "Full name, e.g. John Doe",
|
||||
"email_prompt": "Email, e.g. jdoe@example.com",
|
||||
@@ -64,6 +65,7 @@
|
||||
"sidepanel_title_newtpk": "Start New Chat",
|
||||
"sidepanel_title_cred": "Confirm Credentials",
|
||||
"sidepanel_title_reset": "Reset Password",
|
||||
"sidepanel_title_archive": "Archived Chats",
|
||||
"update_available": "Update available. <a href=\"\">Reload</a>.",
|
||||
"reconnect_countdown": "Disconnected. Reconnecting in {seconds}s…",
|
||||
"reconnect_now": "Try now",
|
||||
@@ -88,6 +90,7 @@
|
||||
"menu_item_unblock": "Unblock",
|
||||
"menu_item_block": "Block",
|
||||
"menu_item_member_delete": "Remove",
|
||||
"menu_item_archive_topic": "Archive",
|
||||
"action_cancel": "cancel",
|
||||
"upload_finishing": "finishing...",
|
||||
"no_contacts": "You have no contacts :-(",
|
||||
@@ -100,6 +103,10 @@
|
||||
"label_file_name": "File name:",
|
||||
"label_content_type": "Content type:",
|
||||
"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",
|
||||
"group_user_id_prompt": "Group or User ID",
|
||||
"button_subscribe": "Subscribe",
|
||||
@@ -249,6 +256,13 @@
|
||||
"user_not_found": "Не найден",
|
||||
"reconnect_countdown": "Нет связи. Подключение через {seconds}с…",
|
||||
"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.
|
||||
|
||||
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 */
|
||||
import React from 'react';
|
||||
import { FormattedHTMLMessage } from 'react-intl';
|
||||
import { FormattedHTMLMessage, defineMessages } from 'react-intl';
|
||||
|
||||
import ContactList from '../widgets/contact-list.jsx';
|
||||
|
||||
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 {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleAction = this.handleAction.bind(this);
|
||||
|
||||
this.state = ContactsView.getDerivedStateFromProps(props, {});
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
let contacts = [];
|
||||
const contacts = [];
|
||||
let unreadThreads = 0;
|
||||
let archivedCount = 0;
|
||||
nextProps.chatList.map((c) => {
|
||||
unreadThreads += c.unread > 0 ? 1 : 0;
|
||||
contacts.push(c);
|
||||
if (c.private && c.private.arch) {
|
||||
if (nextProps.archive) {
|
||||
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);
|
||||
});
|
||||
|
||||
if (archivedCount > 0) {
|
||||
contacts.push({
|
||||
action: 'archive',
|
||||
title: messages.archived_contacts_title,
|
||||
values: {count: archivedCount}
|
||||
});
|
||||
}
|
||||
|
||||
updateFavicon(unreadThreads);
|
||||
|
||||
return {contactList: contacts};
|
||||
return {
|
||||
contactList: contacts,
|
||||
archivedCount: archivedCount
|
||||
};
|
||||
}
|
||||
|
||||
handleAction(action_ignored) {
|
||||
this.props.onShowArchive();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<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">{
|
||||
(no_contacts) => <ContactList
|
||||
connected={this.props.connected}
|
||||
@@ -43,7 +77,8 @@ export default class ContactsView extends React.Component {
|
||||
showOnline={true}
|
||||
showUnread={true}
|
||||
onTopicSelected={this.props.onTopicSelected}
|
||||
showContextMenu={this.props.showContextMenu} />
|
||||
showContextMenu={this.props.showContextMenu}
|
||||
onAction={this.handleAction} />
|
||||
}</FormattedHTMLMessage>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export default class EditAccountView extends React.Component {
|
||||
|
||||
handleFullNameUpdate(fn) {
|
||||
this.setState({fullName: fn});
|
||||
this.props.onUpdateAccount(undefined, vcard(fn, this.state.avatar));
|
||||
this.props.onUpdateAccount(undefined, vcard(fn, null));
|
||||
}
|
||||
|
||||
handlePasswordUpdate(pwd) {
|
||||
@@ -70,7 +70,7 @@ export default class EditAccountView extends React.Component {
|
||||
|
||||
handleImageChanged(img) {
|
||||
this.setState({avatar: img});
|
||||
this.props.onUpdateAccount(undefined, vcard(this.state.fullName, img));
|
||||
this.props.onUpdateAccount(undefined, vcard(null, img));
|
||||
}
|
||||
|
||||
handleCheckboxClick(what, checked) {
|
||||
|
||||
+7
-13
@@ -98,7 +98,7 @@ class InfoView extends React.Component {
|
||||
|
||||
// No need to separately handle component mount.
|
||||
componentDidUpdate(props) {
|
||||
let topic = this.props.tinode.getTopic(props.topic);
|
||||
const topic = this.props.tinode.getTopic(props.topic);
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
@@ -126,7 +126,7 @@ class InfoView extends React.Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
var topic = this.props.tinode.getTopic(this.props.topic);
|
||||
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
@@ -185,7 +185,7 @@ class InfoView extends React.Component {
|
||||
}
|
||||
|
||||
onMetaDesc(desc) {
|
||||
var topic = this.props.tinode.getTopic(this.props.topic);
|
||||
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
@@ -197,7 +197,7 @@ class InfoView extends React.Component {
|
||||
}
|
||||
|
||||
onSubsUpdated() {
|
||||
var topic = this.props.tinode.getTopic(this.props.topic);
|
||||
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
@@ -220,26 +220,20 @@ class InfoView extends React.Component {
|
||||
fn = fn.trim();
|
||||
if (this.state.fullName !== fn) {
|
||||
this.setState({fullName: fn});
|
||||
this.props.onTopicDescUpdate(this.props.topic,
|
||||
vcard(fn, this.state.avatar),
|
||||
null);
|
||||
this.props.onTopicDescUpdate(this.props.topic, vcard(fn, null), null);
|
||||
}
|
||||
}
|
||||
|
||||
handlePrivateUpdate(priv) {
|
||||
if (this.state.priv !== priv) {
|
||||
this.setState({private: priv});
|
||||
this.props.onTopicDescUpdate(this.props.topic,
|
||||
null,
|
||||
priv || Tinode.DEL_CHAR);
|
||||
this.props.onTopicDescUpdate(this.props.topic, null, priv || Tinode.DEL_CHAR);
|
||||
}
|
||||
}
|
||||
|
||||
handleImageChanged(img) {
|
||||
this.setState({avatar: img});
|
||||
this.props.onTopicDescUpdate(this.props.topic,
|
||||
vcard(this.state.fullName, img),
|
||||
null);
|
||||
this.props.onTopicDescUpdate(this.props.topic, vcard(null, img), null);
|
||||
}
|
||||
|
||||
handleMuted(ignored, checked) {
|
||||
|
||||
@@ -12,12 +12,12 @@ export default class LogoView extends React.PureComponent {
|
||||
return (
|
||||
<div id="dummy-view" className={this.props.hideSelf ? 'nodisplay' : null}>
|
||||
<div>
|
||||
<a href="https://github.com/tinode/chat/">
|
||||
<img id="logo" alt="logo" src="img/logo.svg" />
|
||||
<h2>Tinode Web</h2>
|
||||
</a>
|
||||
<p><FormattedMessage id="label_client" defaultMessage="Client:" /> {version}</p>
|
||||
<p><FormattedMessage id="label_server" defaultMessage="Server:" /> {this.props.serverVersion} ({this.props.serverAddress})</p>
|
||||
<a href="https://github.com/tinode/chat/">
|
||||
<img id="logo" alt="logo" src="img/logo.svg" />
|
||||
<h2>Tinode Web</h2>
|
||||
</a>
|
||||
<p><FormattedMessage id="label_client" defaultMessage="Client:" /> {version}</p>
|
||||
<p><FormattedMessage id="label_server" defaultMessage="Server:" /> {this.props.serverVersion} ({this.props.serverAddress})</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+165
-122
@@ -8,6 +8,7 @@ import ChatMessage from '../widgets/chat-message.jsx';
|
||||
import ErrorPanel from '../widgets/error-panel.jsx';
|
||||
import GroupSubs from '../widgets/group-subs.jsx';
|
||||
import ImagePreview from '../widgets/image-preview.jsx';
|
||||
import Invitation from '../widgets/Invitation.jsx';
|
||||
import LetterTile from '../widgets/letter-tile.jsx';
|
||||
import LoadSpinner from '../widgets/load-spinner.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 {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
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.state = MessagesView.getDerivedStateFromProps(props, {});
|
||||
|
||||
this.propsChange = this.propsChange.bind(this);
|
||||
// this.propsChange = this.propsChange.bind(this);
|
||||
this.leave = this.leave.bind(this);
|
||||
this.handleScrollReference = this.handleScrollReference.bind(this);
|
||||
this.handleScrollEvent = this.handleScrollEvent.bind(this);
|
||||
@@ -67,22 +63,11 @@ class MessagesView extends React.Component {
|
||||
this.handleContextClick = this.handleContextClick.bind(this);
|
||||
this.handleShowContextMenuMessage = this.handleShowContextMenuMessage.bind(this);
|
||||
this.handleBackNavigation = this.handleBackNavigation.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;
|
||||
}
|
||||
}
|
||||
this.handleNewChatAcceptance = this.handleNewChatAcceptance.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.propsChange(this.props);
|
||||
// this.propsChange(this.props, this.state);
|
||||
if (this.messagesScroller) {
|
||||
this.messagesScroller.addEventListener('scroll', this.handleScrollEvent);
|
||||
}
|
||||
@@ -95,92 +80,53 @@ class MessagesView extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.propsChange(nextProps);
|
||||
}
|
||||
|
||||
propsChange(props) {
|
||||
if (!props || !props.topic) {
|
||||
let oldTopic = this.state.topic;
|
||||
this.setState({messages: [], onlineSubs: [], topic: null}, this.leave(oldTopic));
|
||||
return;
|
||||
// Scroll last message into view on component update e.g. on message received
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
if (!props.connected) {
|
||||
// connection lost, clear online subs
|
||||
this.setState({onlineSubs: []});
|
||||
return;
|
||||
const topic = this.props.tinode.getTopic(this.state.topic);
|
||||
if (this.state.topic != prevState.topic) {
|
||||
if (prevState.topic) {
|
||||
this.leave(prevState.topic);
|
||||
this.props.readTimerHandler(null);
|
||||
}
|
||||
|
||||
if (topic) {
|
||||
topic.onData = this.handleNewMessage;
|
||||
topic.onAllMessagesReceived = this.handleAllMessagesReceived;
|
||||
topic.onInfo = this.handleInfoReceipt;
|
||||
topic.onMetaDesc = this.handleDescChange;
|
||||
topic.onSubsUpdated = this.handleSubsUpdated;
|
||||
topic.onPres = this.handleSubsUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
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.onAllMessagesReceived = this.handleAllMessagesReceived;
|
||||
topic.onInfo = this.handleInfoReceipt;
|
||||
topic.onMetaDesc = this.handleDescChange;
|
||||
topic.onSubsUpdated = 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({
|
||||
readOnly: props.acs ? !props.acs.isWriter() : true,
|
||||
writeOnly: props.acs ? !props.acs.isReader() : true
|
||||
});
|
||||
|
||||
if (!topic.isSubscribed() && tryToResubscribe) {
|
||||
if (topic && !topic.isSubscribed() &&
|
||||
((!prevProps.connected && this.props.connected) || (this.state.topic != prevState.topic))) {
|
||||
// Don't request the tags. They are useless unless the user
|
||||
// is the owner and is editing the topic.
|
||||
let getQuery = topic.startMetaQuery()
|
||||
const getQuery = topic.startMetaQuery()
|
||||
.withLaterDesc()
|
||||
.withLaterSub()
|
||||
.withLaterData(MESSAGES_PAGE)
|
||||
.withLaterDel();
|
||||
let setQuery = newGroupTopic ? props.newGroupTopicParams : undefined;
|
||||
const setQuery = Tinode.isNewGroupTopicName(this.props.topic) ?
|
||||
this.props.newGroupTopicParams : undefined;
|
||||
// Show "loading" spinner.
|
||||
this.setState({ fetchingMessages: true });
|
||||
topic.subscribe(getQuery.build(), setQuery)
|
||||
.then((ctrl) => {
|
||||
this.setState({topic: ctrl.topic});
|
||||
this.props.onNewTopicCreated(props.topic, ctrl.topic);
|
||||
if (this.state.topic != ctrl.topic) {
|
||||
this.setState({topic: ctrl.topic});
|
||||
}
|
||||
this.props.onNewTopicCreated(this.props.topic, ctrl.topic);
|
||||
// If there are unsent messages, try sending them now.
|
||||
topic.queuedMessages((pub) => {
|
||||
if (!pub._sending && topic.isSubscribed()) {
|
||||
@@ -189,23 +135,107 @@ class MessagesView extends React.Component {
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('Failed subscription to', this.state.topic);
|
||||
this.props.onError(err.message, 'err');
|
||||
const {formatMessage} = this.props.intl;
|
||||
this.setState({
|
||||
title: formatMessage(messages.not_found),
|
||||
avatar: null,
|
||||
readOnly: true,
|
||||
writeOnly: true,
|
||||
fetchingMessages: false,
|
||||
messages: [],
|
||||
onlineSubs: [],
|
||||
typingIndicator: false,
|
||||
imagePreview: null
|
||||
});
|
||||
const blankState = MessagesView.getDerivedStateFromProps({}, {});
|
||||
blankState.title = this.props.intl.formatMessage(messages.not_found);
|
||||
this.setState(blankState);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!oldTopicName) {
|
||||
return;
|
||||
@@ -268,15 +298,19 @@ class MessagesView extends React.Component {
|
||||
avatar: null
|
||||
});
|
||||
}
|
||||
|
||||
if (desc.acs) {
|
||||
this.setState({
|
||||
readOnly: !desc.acs.isWriter(),
|
||||
writeOnly: !desc.acs.isReader()
|
||||
writeOnly: !desc.acs.isReader(),
|
||||
unconfirmed: isUnconfirmed(desc.acs),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleSubsUpdated() {
|
||||
console.log("handleSubsUpdated");
|
||||
|
||||
if (this.state.topic) {
|
||||
var subs = [];
|
||||
var topic = this.props.tinode.getTopic(this.state.topic);
|
||||
@@ -387,10 +421,10 @@ class MessagesView extends React.Component {
|
||||
|
||||
handleShowContextMenuMessage(params) {
|
||||
params.topicName = this.state.topic;
|
||||
var menuItems = ['message_delete'];
|
||||
var topic = this.props.tinode.getTopic(params.topicName);
|
||||
const menuItems = ['message_delete'];
|
||||
const topic = this.props.tinode.getTopic(params.topicName);
|
||||
if (topic) {
|
||||
var acs = topic.getAccessMode();
|
||||
const acs = topic.getAccessMode();
|
||||
if (acs && acs.isDeleter()) {
|
||||
menuItems.push('message_delete_hard');
|
||||
}
|
||||
@@ -402,6 +436,10 @@ class MessagesView extends React.Component {
|
||||
this.props.onHideMessagesView();
|
||||
}
|
||||
|
||||
handleNewChatAcceptance(action) {
|
||||
this.props.onNewChat(this.state.topic, action);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
@@ -526,8 +564,9 @@ class MessagesView extends React.Component {
|
||||
<ul id="scroller" className={chatBoxClass}>
|
||||
{messageNodes}
|
||||
</ul>
|
||||
<div>Typing notifications here.</div>
|
||||
</div>
|
||||
{this.state.writeOnly ?
|
||||
{this.state.writeOnly ?
|
||||
<div id="write-only-background">
|
||||
<div id="write-only-note">
|
||||
<FormattedMessage id="messages_not_readable" defaultMessage="no access to messages"
|
||||
@@ -536,14 +575,18 @@ class MessagesView extends React.Component {
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<SendMessage
|
||||
tinode={this.props.tinode}
|
||||
topic={this.props.topic}
|
||||
disabled={this.state.readOnly}
|
||||
sendMessage={this.props.sendMessage}
|
||||
onError={this.props.onError} />
|
||||
{this.state.unconfirmed ?
|
||||
<Invitation onAction={this.handleNewChatAcceptance} />
|
||||
:
|
||||
<SendMessage
|
||||
tinode={this.props.tinode}
|
||||
topic={this.props.topic}
|
||||
disabled={this.state.readOnly}
|
||||
sendMessage={this.props.sendMessage}
|
||||
onError={this.props.onError} />
|
||||
}
|
||||
{this.state.imagePreview ?
|
||||
<ImagePreview content={this.state.imagePreview}
|
||||
onClose={this.handleCloseImagePreview} /> : null}
|
||||
|
||||
@@ -49,6 +49,11 @@ const messages = defineMessages({
|
||||
id: 'sidepanel_title_reset',
|
||||
description: 'Sidepanel title for PasswordResetView.',
|
||||
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}
|
||||
onError={this.props.onError} /> :
|
||||
|
||||
view === 'contacts' ?
|
||||
(view === 'contacts' || view == 'archive') ?
|
||||
<ContactsView
|
||||
tinode={this.props.tinode}
|
||||
myUserId={this.props.myUserId}
|
||||
connected={this.props.connected}
|
||||
topicSelected={this.props.topicSelected}
|
||||
archive={view == 'archive'}
|
||||
chatList={this.props.chatList}
|
||||
showContextMenu={this.props.showContextMenu}
|
||||
onTopicSelected={this.props.onTopicSelected} /> :
|
||||
onTopicSelected={this.props.onTopicSelected}
|
||||
onShowArchive={this.props.onShowArchive} /> :
|
||||
|
||||
view === 'newtpk' ?
|
||||
<NewTopicView
|
||||
|
||||
+104
-27
@@ -74,12 +74,14 @@ class TinodeWeb extends React.Component {
|
||||
this.handleTopicSelected = this.handleTopicSelected.bind(this);
|
||||
this.handleHideMessagesView = this.handleHideMessagesView.bind(this);
|
||||
this.handleSendMessage = this.handleSendMessage.bind(this);
|
||||
this.handleNewChatInvitation = this.handleNewChatInvitation.bind(this);
|
||||
this.handleNewAccount = this.handleNewAccount.bind(this);
|
||||
this.handleNewAccountRequest = this.handleNewAccountRequest.bind(this);
|
||||
this.handleUpdateAccountRequest = this.handleUpdateAccountRequest.bind(this);
|
||||
this.handleUpdateAccountTagsRequest = this.handleUpdateAccountTagsRequest.bind(this);
|
||||
this.handleSettings = this.handleSettings.bind(this);
|
||||
this.handleGlobalSettings = this.handleGlobalSettings.bind(this);
|
||||
this.handleShowArchive = this.handleShowArchive.bind(this);
|
||||
this.handleToggleMessageSounds = this.handleToggleMessageSounds.bind(this);
|
||||
this.initDesktopAlerts = this.initDesktopAlerts.bind(this);
|
||||
this.togglePushToken = this.togglePushToken.bind(this);
|
||||
@@ -103,10 +105,11 @@ class TinodeWeb extends React.Component {
|
||||
this.handleValidateCredentialsRequest = this.handleValidateCredentialsRequest.bind(this);
|
||||
this.handlePasswordResetRequest = this.handlePasswordResetRequest.bind(this);
|
||||
this.handleResetPassword = this.handleResetPassword.bind(this);
|
||||
this.handleContextMenuAction = this.handleContextMenuAction.bind(this);
|
||||
}
|
||||
|
||||
getBlankState() {
|
||||
let settings = LocalStorageUtil.getObject("settings") || {};
|
||||
const settings = LocalStorageUtil.getObject('settings') || {};
|
||||
|
||||
return {
|
||||
connected: false,
|
||||
@@ -119,7 +122,7 @@ class TinodeWeb extends React.Component {
|
||||
desktopAlertsEnabled: (isSecureConnection() || isLocalHost()) &&
|
||||
(typeof firebase != 'undefined') && (typeof navigator != 'undefined') &&
|
||||
(typeof FIREBASE_INIT != 'undefined'),
|
||||
firebaseToken: LocalStorageUtil.getObject("firebase-token"),
|
||||
firebaseToken: LocalStorageUtil.getObject('firebase-token'),
|
||||
|
||||
errorText: '',
|
||||
errorLevel: null,
|
||||
@@ -192,7 +195,7 @@ class TinodeWeb extends React.Component {
|
||||
if (!this.state.firebaseToken) {
|
||||
this.togglePushToken(true);
|
||||
} else {
|
||||
this.tinode.setDeviceToken(this.state.firebaseToken);
|
||||
this.tinode.setDeviceToken(this.state.firebaseToken, true);
|
||||
}
|
||||
}
|
||||
}).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;
|
||||
|
||||
let parsedNav = HashNavigation.parseUrlHash(window.location.hash);
|
||||
const parsedNav = HashNavigation.parseUrlHash(window.location.hash);
|
||||
if (token) {
|
||||
// When reading from storage, date is returned as string.
|
||||
token.expires = new Date(token.expires);
|
||||
@@ -274,7 +277,7 @@ class TinodeWeb extends React.Component {
|
||||
var hash = HashNavigation.parseUrlHash(window.location.hash);
|
||||
if (hash.path && hash.path.length > 0) {
|
||||
// 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]});
|
||||
} else {
|
||||
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
|
||||
// with attachments.
|
||||
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);
|
||||
// The uploader is used to show progress.
|
||||
@@ -716,11 +719,51 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
topic.publishDraft(msg, promise)
|
||||
.then((ctrl) => {
|
||||
if (topic.isArchived()) {
|
||||
return topic.archive(false);
|
||||
}
|
||||
})
|
||||
.catch((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.
|
||||
handleNewAccount() {
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, 'register'));
|
||||
@@ -745,7 +788,14 @@ class TinodeWeb extends React.Component {
|
||||
|
||||
handleUpdateAccountRequest(password, 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');
|
||||
});
|
||||
}
|
||||
@@ -793,12 +843,18 @@ class TinodeWeb extends React.Component {
|
||||
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.
|
||||
initDesktopAlerts() {
|
||||
// Google could not be bothered to mention that
|
||||
// onTokenRefresh is never called.
|
||||
this.fbPush.onTokenRefresh(() => {
|
||||
this.requestPushToken(true);
|
||||
this.requestPushToken();
|
||||
});
|
||||
|
||||
this.fbPush.onMessage((payload) => {
|
||||
@@ -811,7 +867,7 @@ class TinodeWeb extends React.Component {
|
||||
if (enabled) {
|
||||
if (!this.state.firebaseToken) {
|
||||
this.fbPush.requestPermission().then(() => {
|
||||
this.requestPushToken(true);
|
||||
this.requestPushToken();
|
||||
}).catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
this.setState({desktopAlerts: false, firebaseToken: null});
|
||||
@@ -836,10 +892,10 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
requestPushToken(sendToServer) {
|
||||
requestPushToken() {
|
||||
this.fbPush.getToken().then((refreshedToken) => {
|
||||
if (refreshedToken != this.state.firebaseToken) {
|
||||
this.tinode.setDeviceToken(refreshedToken, sendToServer);
|
||||
this.tinode.setDeviceToken(refreshedToken, true);
|
||||
LocalStorageUtil.setObject('firebase-token', refreshedToken);
|
||||
}
|
||||
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.
|
||||
handleSidepanelCancel() {
|
||||
var parsed = HashNavigation.parseUrlHash(window.location.hash);
|
||||
const parsed = HashNavigation.parseUrlHash(window.location.hash);
|
||||
parsed.path[0] = this.state.myUserId ? 'contacts' : '';
|
||||
if (parsed.params) {
|
||||
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.
|
||||
handleNewTopicRequest(peerName, pub, priv, tags) {
|
||||
var topicName = peerName || this.tinode.newGroupTopicName();
|
||||
const topicName = peerName || this.tinode.newGroupTopicName();
|
||||
if (!peerName) {
|
||||
this.setState(
|
||||
{newGroupTopicParams: {desc: {public: pub, private: {comment: priv}}, tags: tags}},
|
||||
@@ -938,9 +994,8 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
handleTagsUpdated(topicName, tags) {
|
||||
var topic = this.tinode.getTopic(topicName);
|
||||
const topic = this.tinode.getTopic(topicName);
|
||||
if (topic) {
|
||||
var instance = this;
|
||||
topic.setMeta({tags: tags}).catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
@@ -949,7 +1004,15 @@ class TinodeWeb extends React.Component {
|
||||
|
||||
handleLogout() {
|
||||
updateFavicon(0);
|
||||
|
||||
// Remove stored data.
|
||||
localStorage.removeItem('auth-token');
|
||||
localStorage.removeItem('firebase-token');
|
||||
localStorage.removeItem('settings');
|
||||
if (this.state.firebaseToken) {
|
||||
this.fbPush.deleteToken(this.state.firebaseToken)
|
||||
}
|
||||
|
||||
if (this.tinode) {
|
||||
this.tinode.onDisconnect = undefined;
|
||||
this.tinode.disconnect();
|
||||
@@ -962,7 +1025,7 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
handleLeaveUnsubRequest(topicName) {
|
||||
var topic = this.tinode.getTopic(topicName);
|
||||
const topic = this.tinode.getTopic(topicName);
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
@@ -993,15 +1056,16 @@ class TinodeWeb extends React.Component {
|
||||
const topic = this.tinode.getTopic(topicName);
|
||||
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.isSubscribed()) {
|
||||
subscribed = true;
|
||||
subscribed = topic.isSubscribed();
|
||||
archived = topic.isArchived();
|
||||
|
||||
let acs = topic.getAccessMode();
|
||||
muted = acs && acs.isMuted();
|
||||
blocked = acs && !acs.isJoiner();
|
||||
deleter = acs && acs.isDeleter();
|
||||
const acs = topic.getAccessMode();
|
||||
if (acs) {
|
||||
muted = acs.isMuted();
|
||||
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 ? 'messages_clear' : null,
|
||||
subscribed && deleter ? 'messages_clear_hard' : null,
|
||||
subscribed ? (muted ? 'topic_unmute' : 'topic_mute') : null,
|
||||
subscribed ? (blocked ? 'topic_unblock' : 'topic_block') : null,
|
||||
muted ? 'topic_unmute' : 'topic_mute',
|
||||
blocked ? 'topic_unblock' : 'topic_block',
|
||||
!archived ? 'topic_archive' : null,
|
||||
'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() {
|
||||
HashNavigation.navigateTo(HashNavigation.addUrlParam(window.location.hash, 'info', true));
|
||||
this.setState({showInfoPanel: true});
|
||||
@@ -1039,7 +1114,7 @@ class TinodeWeb extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
var topic = this.tinode.getTopic(topicName);
|
||||
const topic = this.tinode.getTopic(topicName);
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
@@ -1157,6 +1232,7 @@ class TinodeWeb extends React.Component {
|
||||
onValidateCredentials={this.handleValidateCredentialsRequest}
|
||||
onPasswordResetRequest={this.handlePasswordResetRequest}
|
||||
onResetPassword={this.handleResetPassword}
|
||||
onShowArchive={this.handleShowArchive}
|
||||
|
||||
onInitFind={this.tnInitFind}
|
||||
searchResults={this.state.searchResults}
|
||||
@@ -1192,6 +1268,7 @@ class TinodeWeb extends React.Component {
|
||||
onNewTopicCreated={this.handleNewTopicCreated}
|
||||
readTimerHandler={this.handleReadTimer}
|
||||
showContextMenu={this.handleShowContextMenu}
|
||||
onNewChat={this.handleNewChatInvitation}
|
||||
sendMessage={this.handleSendMessage} />
|
||||
|
||||
{this.state.showInfoPanel ?
|
||||
|
||||
@@ -186,7 +186,6 @@ function draftyFormatter(style, data, values, key) {
|
||||
case 'FM':
|
||||
// Form
|
||||
attr.className = 'bot-form';
|
||||
// console.log(data, attr);
|
||||
break;
|
||||
case 'FE':
|
||||
// Form element formatting is dependent on element content.
|
||||
|
||||
+20
-21
@@ -9,13 +9,7 @@ export default class ChipInput extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
placeholder: props.chips ? '' : props.prompt,
|
||||
sortedChips: ChipInput.sortChips(props.chips, props.required),
|
||||
chipIndex: ChipInput.indexChips(props.chips),
|
||||
input: '',
|
||||
focused: false
|
||||
};
|
||||
this.state = ChipInput.getDerivedStateFromProps(props);
|
||||
|
||||
this.handleTextInput = this.handleTextInput.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);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.setState({
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
const state = {
|
||||
placeholder: nextProps.chips ? '' : nextProps.prompt,
|
||||
sortedChips: ChipInput.sortChips(nextProps.chips, nextProps.required),
|
||||
chipIndex: ChipInput.indexChips(nextProps.chips)
|
||||
});
|
||||
if (nextProps.chips.length > this.props.chips.length) {
|
||||
chipIndex: ChipInput.indexChips(nextProps.chips),
|
||||
focused: prevState && prevState.focused
|
||||
};
|
||||
|
||||
if (!prevState || nextProps.chips.length > prevState.sortedChips.length) {
|
||||
// Chip added: clear input.
|
||||
this.setState({input: ''});
|
||||
state.input ='';
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
// Map chip index to user name
|
||||
static indexChips(chips) {
|
||||
var index = {};
|
||||
var count = 0;
|
||||
const index = {};
|
||||
let count = 0;
|
||||
chips.map(function(item) {
|
||||
index[item.user] = count;
|
||||
count ++;
|
||||
@@ -49,8 +48,8 @@ export default class ChipInput extends React.Component {
|
||||
|
||||
// Have non-removable chips appear before all other chips.
|
||||
static sortChips(chips, keep) {
|
||||
var required = [];
|
||||
var normal = [];
|
||||
const required = [];
|
||||
const normal = [];
|
||||
chips.map(function(item) {
|
||||
if (item.user === keep) {
|
||||
required.push(item);
|
||||
@@ -69,7 +68,7 @@ export default class ChipInput extends React.Component {
|
||||
}
|
||||
|
||||
removeChipAt(idx) {
|
||||
var removed = this.state.sortedChips[idx];
|
||||
const removed = this.state.sortedChips[idx];
|
||||
this.props.onChipRemoved(removed.user, this.state.chipIndex[removed.user]);
|
||||
}
|
||||
|
||||
@@ -108,8 +107,8 @@ export default class ChipInput extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
var chips = [];
|
||||
var count = 0;
|
||||
const chips = [];
|
||||
let count = 0;
|
||||
this.state.sortedChips.map((item) => {
|
||||
chips.push(
|
||||
<Chip
|
||||
@@ -124,7 +123,7 @@ export default class ChipInput extends React.Component {
|
||||
);
|
||||
count++;
|
||||
});
|
||||
var className = "chip-input" + (this.state.focused ? ' focused' : '');
|
||||
const className = "chip-input" + (this.state.focused ? ' focused' : '');
|
||||
return (
|
||||
<div className={className}>
|
||||
{chips}
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class Chip extends React.PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
var title = this.props.title || this.props.topic;
|
||||
const title = this.props.title || this.props.topic;
|
||||
return (
|
||||
<div className="chip">
|
||||
{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 Contact from './contact.jsx';
|
||||
import ContactAction from './contact-action.jsx';
|
||||
|
||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
||||
|
||||
@@ -25,69 +26,88 @@ class ContactList extends React.Component {
|
||||
const {formatMessage} = this.props.intl;
|
||||
const showCheckmark = Array.isArray(this.props.topicSelected);
|
||||
const contactNodes = [];
|
||||
let contactsCount = 0;
|
||||
if (this.props.contacts && this.props.contacts.length > 0) {
|
||||
this.props.contacts.map((c) => {
|
||||
let key = c.topic ? c.topic : c.user;
|
||||
// If filter function is provided, filter out the items
|
||||
// which don't satisfy the condition.
|
||||
if (this.props.filterFunc && this.props.filter) {
|
||||
let content = [key];
|
||||
if (c.private && c.private.comment) {
|
||||
content.push(('' + c.private.comment).toLowerCase());
|
||||
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
|
||||
// which don't satisfy the condition.
|
||||
if (this.props.filterFunc && this.props.filter) {
|
||||
let content = [key];
|
||||
if (c.private && c.private.comment) {
|
||||
content.push(('' + c.private.comment).toLowerCase());
|
||||
}
|
||||
if (c.public && c.public.fn) {
|
||||
content.push(('' + c.public.fn).toLowerCase());
|
||||
}
|
||||
if (!this.props.filterFunc(this.props.filter, content)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (c.public && c.public.fn) {
|
||||
content.push(('' + c.public.fn).toLowerCase());
|
||||
}
|
||||
if (!this.props.filterFunc(this.props.filter, content)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let selected = showCheckmark ?
|
||||
(this.props.topicSelected.indexOf(key) > -1) :
|
||||
(this.props.topicSelected === key);
|
||||
let badges = [];
|
||||
if (this.props.showMode) {
|
||||
if (key === this.props.myUserId) {
|
||||
badges.push({name: formatMessage(messages.badge_you), color: 'green'});
|
||||
const selected = showCheckmark ?
|
||||
(this.props.topicSelected.indexOf(key) > -1) :
|
||||
(this.props.topicSelected === key);
|
||||
const badges = [];
|
||||
if (this.props.showMode) {
|
||||
if (key === this.props.myUserId) {
|
||||
badges.push({name: formatMessage(messages.badge_you), color: 'green'});
|
||||
}
|
||||
if (c.acs && c.acs.isOwner()) {
|
||||
badges.push({name: formatMessage(messages.badge_owner), color: 'blue'});
|
||||
}
|
||||
}
|
||||
if (c.acs && c.acs.isOwner()) {
|
||||
badges.push({name: formatMessage(messages.badge_owner), color: 'blue'});
|
||||
}
|
||||
}
|
||||
let comment = Array.isArray(c.private) ?
|
||||
c.private.join(',') : (c.private ? c.private.comment : null);
|
||||
const comment = Array.isArray(c.private) ?
|
||||
c.private.join(',') : (c.private ? c.private.comment : null);
|
||||
|
||||
contactNodes.push(
|
||||
<Contact
|
||||
title={c.public ? c.public.fn : null}
|
||||
avatar={makeImageUrl(c.public ? c.public.photo : null)}
|
||||
comment={comment}
|
||||
unread={this.props.showUnread ? c.unread : 0}
|
||||
now={c.online && this.props.connected}
|
||||
acs={c.acs}
|
||||
showMode={this.props.showMode}
|
||||
badges={badges}
|
||||
showCheckmark={showCheckmark}
|
||||
selected={selected}
|
||||
showOnline={this.props.showOnline}
|
||||
onSelected={this.props.onTopicSelected}
|
||||
showContextMenu={this.props.showContextMenu}
|
||||
item={key}
|
||||
index={contactNodes.length}
|
||||
key={key} />
|
||||
);
|
||||
contactNodes.push(
|
||||
<Contact
|
||||
title={c.public ? c.public.fn : null}
|
||||
avatar={makeImageUrl(c.public ? c.public.photo : null)}
|
||||
comment={comment}
|
||||
unread={this.props.showUnread ? c.unread : 0}
|
||||
now={c.online && this.props.connected}
|
||||
acs={c.acs}
|
||||
showMode={this.props.showMode}
|
||||
badges={badges}
|
||||
showCheckmark={showCheckmark}
|
||||
selected={selected}
|
||||
showOnline={this.props.showOnline}
|
||||
onSelected={this.props.onTopicSelected}
|
||||
showContextMenu={this.props.showContextMenu}
|
||||
item={key}
|
||||
index={contactNodes.length}
|
||||
key={key} />
|
||||
);
|
||||
contactsCount ++;
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={this.props.noScroll ? null : "scrollable-panel"}>
|
||||
{contactsCount == 0 ?
|
||||
<div className="center-medium-text"
|
||||
dangerouslySetInnerHTML={{__html: this.props.emptyListMessage}} />
|
||||
:
|
||||
null
|
||||
}
|
||||
{contactNodes.length > 0 ?
|
||||
<ul className="contact-box">
|
||||
{contactNodes}
|
||||
</ul>
|
||||
:
|
||||
<div className="center-medium-text" dangerouslySetInnerHTML={{__html: this.props.emptyListMessage}} />
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -69,7 +69,7 @@ export default class Contact extends React.Component {
|
||||
<i className="material-icons">expand_more</i>
|
||||
</a>
|
||||
</span> : null}
|
||||
</li>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
+139
-47
@@ -60,6 +60,11 @@ const messages = defineMessages({
|
||||
id: 'menu_item_member_delete',
|
||||
defaultMessage: 'Remove',
|
||||
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.
|
||||
this.MenuItems = {
|
||||
'topic_info': {title: formatMessage(messages.info), handler: null},
|
||||
|
||||
'messages_clear': {title: formatMessage(messages.clear_messages), handler: (params, errorHandler) => {
|
||||
this.deleteMessages(true, false, params, errorHandler);
|
||||
}},
|
||||
'messages_clear_hard': {title: formatMessage(messages.clear_for_all), handler: (params, errorHandler) => {
|
||||
this.deleteMessages(true, true, params, errorHandler);
|
||||
}},
|
||||
'message_delete': {title: formatMessage(messages.delete), handler: (params, errorHandler) => {
|
||||
this.deleteMessages(false, false, params, errorHandler);
|
||||
}},
|
||||
'message_delete_hard': {title: formatMessage(messages.delete_for_all), handler: (params, errorHandler) => {
|
||||
this.deleteMessages(false, true, params, errorHandler);
|
||||
}},
|
||||
'topic_unmute': {title: formatMessage(messages.unmute), handler: this.topicPermissionSetter.bind(this, '+P')},
|
||||
'topic_mute': {title: formatMessage(messages.mute), handler: this.topicPermissionSetter.bind(this, '-P')},
|
||||
'topic_unblock': {title: formatMessage(messages.unblock), handler: this.topicPermissionSetter.bind(this, '+J')},
|
||||
'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);
|
||||
if (!topic) {
|
||||
console.log("Topic not found: ", params.topicName);
|
||||
return;
|
||||
'topic_info': {
|
||||
id: 'topic_info',
|
||||
title: formatMessage(messages.info),
|
||||
handler: null
|
||||
},
|
||||
'messages_clear': {
|
||||
id: 'messages_clear',
|
||||
title: formatMessage(messages.clear_messages),
|
||||
handler: (params, errorHandler) => {
|
||||
return this.deleteMessages(true, false, params, errorHandler);
|
||||
}
|
||||
topic.delTopic().catch((err) => {
|
||||
if (errorHandler) {
|
||||
errorHandler(err.message, 'err');
|
||||
},
|
||||
'messages_clear_hard': {
|
||||
id: 'messages_clear_hard',
|
||||
title: formatMessage(messages.clear_for_all),
|
||||
handler: (params, errorHandler) => {
|
||||
return this.deleteMessages(true, true, params, errorHandler);
|
||||
}
|
||||
},
|
||||
'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) {
|
||||
console.log("Topic not found: ", params.topicName);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}},
|
||||
return topic.delTopic().catch((err) => {
|
||||
if (errorHandler) {
|
||||
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.
|
||||
'permissions': {title: formatMessage({id: 'menu_item_edit_permissions'}), handler: null},
|
||||
'member_delete': {title: formatMessage(messages.member_delete), handler: (params, errorHandler) => {
|
||||
let topic = this.props.tinode.getTopic(params.topicName);
|
||||
if (!topic || !params.user) {
|
||||
console.log("Topic or user not found: '" + params.topicName + "', '" + params.user + "'");
|
||||
return;
|
||||
}
|
||||
topic.delSubscription(params.user).catch((err) => {
|
||||
if (errorHandler) {
|
||||
errorHandler(err.message, 'err');
|
||||
'permissions': {
|
||||
id: 'permissions',
|
||||
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) {
|
||||
console.log("Topic or user not found: '" + params.topicName + "', '" + params.user + "'");
|
||||
return;
|
||||
}
|
||||
});
|
||||
}},
|
||||
'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_block': {title: formatMessage(messages.block), handler: this.topicPermissionSetter.bind(this, '-J')},
|
||||
'member_unblock': {title: formatMessage(messages.unblock), handler: this.topicPermissionSetter.bind(this, '+J')},
|
||||
return topic.delSubscription(params.user).catch((err) => {
|
||||
if (errorHandler) {
|
||||
errorHandler(err.message, 'err');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
'member_mute': {
|
||||
id: 'member_mute',
|
||||
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') {
|
||||
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
|
||||
@@ -183,7 +275,7 @@ class ContextMenu extends React.Component {
|
||||
topic.delMessagesAll(hard) :
|
||||
topic.delMessagesList([params.seq], hard);
|
||||
|
||||
promise.catch((err) => {
|
||||
return promise.catch((err) => {
|
||||
if (errorHandler) {
|
||||
errorHandler(err.message, 'err');
|
||||
}
|
||||
@@ -210,7 +302,7 @@ class ContextMenu extends React.Component {
|
||||
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) {
|
||||
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 {
|
||||
render() {
|
||||
var showUnreadBadge = null;
|
||||
if (this.props.count > 0) {
|
||||
var count = this.props.count > 9 ? "9+" : this.props.count;
|
||||
showUnreadBadge = <span className="unread">{count}</span>;
|
||||
}
|
||||
return showUnreadBadge;
|
||||
return (this.props.count > 0 ?
|
||||
<span className="unread">{this.props.count > 9 ? "9+" : this.props.count}</span>
|
||||
: null);
|
||||
}
|
||||
};
|
||||
|
||||
+714
-309
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: {
|
||||
rules: [
|
||||
{
|
||||
test: /src\/.*\.jsx?$/,
|
||||
test: /src\/.*\.(js|jsx)?$/,
|
||||
loaders: [
|
||||
'babel-loader',
|
||||
],
|
||||
@@ -30,6 +30,10 @@ module.exports = {
|
||||
})
|
||||
]
|
||||
},
|
||||
performance: {
|
||||
maxEntrypointSize: 280000,
|
||||
maxAssetSize: 280000
|
||||
},
|
||||
externals: {
|
||||
'react': 'React',
|
||||
'react-dom': 'ReactDOM',
|
||||
|
||||
Reference in New Issue
Block a user