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;
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 {
+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/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
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/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>
+1670 -2987
View File
File diff suppressed because it is too large Load Diff
+14 -12
View File
@@ -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
View File
@@ -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) => {
+7
View File
@@ -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
View File
@@ -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": "Заблокировать"
}
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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";
+43 -8
View File
@@ -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>
);
}
+2 -2
View File
@@ -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
View File
@@ -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) {
+6 -6
View File
@@ -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
View File
@@ -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}
+9 -2
View File
@@ -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
View File
@@ -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 ?
-1
View File
@@ -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
View File
@@ -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}
+1 -1
View File
@@ -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 ?
+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);
+67 -47
View File
@@ -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>
);
+1 -1
View File
@@ -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
View File
@@ -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');
}
+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 {
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
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: {
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',